-
1
require 'open3'
-
-
1
class Bash
-
-
1
def run(command)
-
250
stdout,stderr,r = Open3.capture3(command)
-
248
[ stdout, stderr, r.exitstatus ]
-
end
-
-
end
-
-
1
module Empty
-
1
def self.binding
-
68
super
-
end
-
end
-
# frozen_string_literal: true
-
1
require_relative 'bash'
-
1
require_relative 'log'
-
1
require_relative 'shell'
-
-
1
class Externals
-
-
1
def initialize(options = {})
-
86
@bash = options['bash'] || Bash.new
-
86
@log = options['log'] || Log.new
-
86
@shell = Shell.new(self)
-
end
-
-
1
attr_reader :bash, :log, :shell
-
-
end
-
# frozen_string_literal: true
-
1
module FilesDelta
-
-
1
def files_delta(was, now)
-
76
changed = {}
-
76
deleted = []
-
76
was.each do |filename, content|
-
236
if !now.has_key?(filename)
-
13
deleted << filename
-
223
elsif now[filename]['content'] != content
-
3
changed[filename] = now[filename]
-
end
-
236
now.delete(filename) # destructive
-
end
-
76
created = now
-
76
[created,deleted.sort,changed]
-
end
-
-
end
-
1
require 'stringio'
-
1
require 'zlib'
-
-
1
module Gnu
-
-
1
def self.unzip(s)
-
72
reader = Zlib::GzipReader.new(StringIO.new(s))
-
72
unzipped = reader.read
-
72
reader.close
-
72
unzipped
-
end
-
-
end
-
1
require 'stringio'
-
1
require 'zlib'
-
-
1
module Gnu
-
-
1
def self.zip(s)
-
80
zipped = StringIO.new('')
-
80
writer = Zlib::GzipWriter.new(zipped)
-
80
writer.write(s)
-
80
writer.close
-
80
zipped.string
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'json'
-
-
1
class HttpJsonArgs
-
-
1
class Error < RuntimeError
-
1
def initialize(message)
-
28
super
-
end
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def initialize(body)
-
35
@args = json_parse(body)
-
29
unless @args.is_a?(Hash)
-
12
raise request_error('body is not JSON Hash')
-
end
-
rescue JSON::ParserError
-
6
raise request_error('body is not JSON')
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def get(path)
-
17
case path
-
1
when '/sha' then ['sha',[]]
-
1
when '/alive' then ['alive?',[]]
-
2
when '/ready' then ['ready?',[]]
-
7
when '/run_cyber_dojo_sh' then ['run_cyber_dojo_sh',[image_name, id, files, max_seconds]]
-
else
-
6
raise request_error('unknown path')
-
end
-
end
-
-
1
private
-
-
1
def json_parse(body)
-
35
if body === ''
-
1
{}
-
else
-
34
JSON.parse(body)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def image_name
-
7
exists_arg('image_name')
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def id
-
6
exists_arg('id')
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def files
-
5
exists_arg('files')
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def max_seconds
-
4
exists_arg('max_seconds')
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def exists_arg(name)
-
22
unless @args.has_key?(name)
-
4
raise missing(name)
-
end
-
18
arg = @args[name]
-
18
arg
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def missing(arg_name)
-
4
request_error("#{arg_name} is missing")
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def request_error(text)
-
# Exception messages use the words 'body' and 'path'
-
# to match RackDispatcher's exception keys.
-
28
HttpJsonArgs::Error.new(text)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
class Log
-
-
1
def <<(message)
-
12
$stdout.print(message)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'http_json_args'
-
1
require_relative 'runner'
-
1
require 'rack'
-
1
require 'json'
-
-
1
class RackDispatcher
-
-
1
def initialize(externals)
-
35
@externals = externals
-
end
-
-
1
def call(env, request_class = Rack::Request)
-
35
request = request_class.new(env)
-
35
path = request.path_info
-
35
body = request.body.read
-
35
name,args = HttpJsonArgs.new(body).get(path)
-
7
runner = Runner.new(@externals)
-
7
result = runner.public_send(name, *args)
-
6
json_response_pass(200, result)
-
rescue HttpJsonArgs::Error => error
-
28
json_response_fail(400, diagnostic(path, body, error))
-
rescue Exception => error
-
1
json_response_fail(500, diagnostic(path, body, error))
-
end
-
-
1
private
-
-
1
def json_response_pass(status, json)
-
6
s = JSON.fast_generate(json)
-
6
[ status, CONTENT_TYPE_JSON, [s] ]
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def json_response_fail(status, json)
-
29
s = JSON.pretty_generate(json)
-
29
$stderr.puts(s)
-
29
$stderr.flush
-
29
[ status, CONTENT_TYPE_JSON, [s] ]
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def diagnostic(path, body, error)
-
29
{ 'exception' => {
-
'path' => path,
-
'body' => body,
-
'class' => 'RunnerService',
-
'message' => error.message,
-
'backtrace' => error.backtrace
-
}
-
}
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
CONTENT_TYPE_JSON = { 'Content-Type' => 'application/json' }
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'files_delta'
-
1
require_relative 'gnu_unzip'
-
1
require_relative 'gnu_zip'
-
1
require_relative 'tar_reader'
-
1
require_relative 'tar_writer'
-
1
require_relative 'traffic_light'
-
1
require_relative 'utf8_clean'
-
1
require 'securerandom'
-
1
require 'timeout'
-
-
1
class Runner
-
-
1
def initialize(externals)
-
71
@externals = externals
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def sha
-
2
{ 'sha' => ENV['SHA'] }
-
end
-
-
1
def alive?
-
2
{ 'alive?' => true }
-
end
-
-
1
def ready?
-
3
{ 'ready?' => true }
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def run_cyber_dojo_sh(image_name, id, files, max_seconds)
-
# [X] Assumes image_name was built by image_builder with a
-
# Dockerfile augmented by image_dockerfile_augmenter. See
-
# https://github.com/cyber-dojo-languages/image_builder
-
# https://github.com/cyber-dojo-languages/image_dockerfile_augmenter
-
80
@result = {}
-
80
create_container(image_name, id, max_seconds+2)
-
begin
-
79
run(files, max_seconds)
-
79
text_file_changes(files)
-
79
traffic_light(image_name, id)
-
79
@result
-
ensure
-
79
remove_container
-
end
-
end
-
-
1
private
-
-
1
include FilesDelta
-
1
include TrafficLight
-
-
1
KB = 1024
-
1
MB = 1024 * KB
-
1
GB = 1024 * MB
-
-
1
SANDBOX_DIR = '/sandbox' # where files are saved to in the container
-
1
UID = 41966 # sandbox user - runs /sandbox/cyber-dojo.sh
-
1
GID = 51966 # sandbox group - runs /sandbox/cyber-dojo.sh
-
1
MAX_FILE_SIZE = 50 * KB # of stdout, stderr, created, changed
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def run(files, max_seconds)
-
79
command = main_docker_run_command
-
79
stdout,stderr,status,timed_out = nil,nil,nil,nil
-
79
r_stdin, w_stdin = IO.pipe
-
79
r_stdout, w_stdout = IO.pipe
-
79
r_stderr, w_stderr = IO.pipe
-
79
w_stdin.write(tgz(files))
-
79
w_stdin.close
-
79
pid = Process.spawn(command, {
-
pgroup:true, # become process leader
-
in:r_stdin, # redirection
-
out:w_stdout, # redirection
-
err:w_stderr # redirection
-
})
-
begin
-
79
Timeout::timeout(max_seconds) do
-
79
_, ps = Process.waitpid2(pid)
-
76
status = ps.exitstatus
-
76
timed_out = killed?(status)
-
end
-
rescue Timeout::Error
-
3
Process_kill_group(pid)
-
3
Process_detach(pid)
-
3
status = KILLED_STATUS
-
3
timed_out = true
-
ensure
-
79
w_stdout.close unless w_stdout.closed?
-
79
w_stderr.close unless w_stderr.closed?
-
79
stdout = packaged(read_max(r_stdout))
-
79
stderr = packaged(read_max(r_stderr))
-
79
r_stdout.close
-
79
r_stderr.close
-
end
-
79
@result['run_cyber_dojo_sh'] = {
-
stdout:stdout,
-
stderr:stderr,
-
status:status,
-
timed_out:timed_out
-
}
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def tgz(files)
-
79
Gnu.zip(Tar::Writer.new(files).tar_file)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def read_max(fd)
-
158
fd.read(MAX_FILE_SIZE + 1) || ''
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def main_docker_run_command
-
# Assumes a tgz of files on stdin. Untars this into the
-
# /sandbox/ dir (which must exist [X]) inside the container
-
# and runs /sandbox/cyber-dojo.sh
-
#
-
# [1] The uid/gid are for the user/group called sandbox [X].
-
# Untars files as this user to set their ownership.
-
# [2] Don't use [docker exec --workdir] as that requires API version
-
# 1.35 but CircleCI is currently using Docker Daemon API 1.32
-
# [3] tar is installed [X].
-
# [4] tar has the --touch option installed [X].
-
# (not true in a default Alpine container)
-
# --touch means 'dont extract file modified time'
-
# It relates to the files modification-date (stat %y).
-
# Without it the untarred files may all end up with the same
-
# modification date. With it, the untarred files have a
-
# proper date-time file-stamp in all supported OS's.
-
# [5] tar date-time file-stamps have a granularity < 1 second [X].
-
# In a default Alpine container the date-time file-stamps
-
# have a granularity of one second; viz, the microseconds
-
# value is always zero.
-
79
<<~SHELL.strip
-
docker exec \
-
--interactive `# piping stdin` \
-
--user=#{UID}:#{GID} `# [1]` \
-
#{@container_name} \
-
bash -c \
-
' `# open quote` \
-
cd #{SANDBOX_DIR} `# [2]` \
-
&& \
-
tar `# [3]` \
-
--touch `# [4][5]` \
-
-zxf `# extract tgz file` \
-
- `# read from stdin` \
-
&& \
-
bash ./cyber-dojo.sh \
-
' `# close quote`
-
SHELL
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def text_file_changes(files)
-
# Approval-style test-frameworks compare actual-text against
-
# expected-text held inside a 'golden-master' file and, if the
-
# comparison fails, generate a file holding the actual-text
-
# ready for human inspection. cyber-dojo supports this by
-
# tar-piping out all text files (generated inside the container)
-
# under /sandbox after cyber-dojo.sh has run.
-
#
-
# [1] Extract /usr/local/bin/red_amber_green.rb if it exists.
-
# [2] Ensure filenames are not read as tar command options.
-
# Eg -J... is a tar compression option.
-
# This option is not available on Ubuntu 16.04
-
79
rag_filename = SecureRandom.urlsafe_base64
-
docker_tar_pipe_text_files_out =
-
79
<<~SHELL.strip
-
docker exec \
-
--user=#{UID}:#{GID} \
-
#{@container_name} \
-
bash -c \
-
' `# open quote` \
-
#{copy_rag(rag_filename)} `# [1]`; \
-
#{ECHO_TRUNCATED_TEXTFILE_NAMES} \
-
| \
-
tar \
-
-C \
-
#{SANDBOX_DIR} \
-
-zcf `# create tgz file` \
-
- `# write to stdout` \
-
--verbatim-files-from `# [2]` \
-
-T `# using filenames` \
-
- `# from stdin` \
-
' `# close quote`
-
SHELL
-
# A crippled container (eg fork-bomb) will likely
-
# not be running causing the [docker exec] to fail.
-
# Be careful if you switch to shell.assert() here.
-
79
stdout,stderr,status = shell.exec(docker_tar_pipe_text_files_out)
-
79
if status === 0
-
71
files_now = read_tar_file(Gnu.unzip(stdout))
-
71
rag_src = extract_rag(files_now, rag_filename)
-
71
created,deleted,changed = *files_delta(files, files_now)
-
else
-
8
@result['diagnostic'] = { 'stderr' => stderr }
-
8
rag_src = nil
-
8
created,deleted,changed = {}, [], {}
-
end
-
79
@result['rag_src'] = rag_src
-
79
@result['run_cyber_dojo_sh'].merge!({
-
created:created,
-
deleted:deleted,
-
changed:changed
-
})
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def copy_rag(rag_filename)
-
79
rag_src = '/usr/local/bin/red_amber_green.rb'
-
79
rag_dst = "#{SANDBOX_DIR}/#{rag_filename}"
-
# This command must not write anything to stdout/stderr
-
# since it would be taken as a filename by tar's -T option.
-
79
"[ -f #{rag_src} ] && cp #{rag_src} #{rag_dst}"
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def extract_rag(files_now, rag_filename)
-
71
rag_file = files_now.delete(rag_filename)
-
71
if rag_file.nil?
-
nil
-
else
-
70
rag_file['content']
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def read_tar_file(tar_file)
-
71
reader = Tar::Reader.new(tar_file)
-
71
reader.files.each_with_object({}) do |(filename,content),memo|
-
597
memo[filename] = packaged(content)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# o) Must not contain a single-quote [bash -c '...']
-
# o) grep -q is --quiet
-
# o) grep -v is --invert-match
-
# o) Strip ./ from front of pathed filename in depathed()
-
# o) The file utility must be installed [X].
-
# However, it incorrectly reports size==0,1 as binary
-
# which is impossible. No executable binary can be that small.
-
# o) truncates text files to MAX_FILE_SIZE+1
-
# This is so truncated?() can detect the truncation.
-
# The truncate utility must be installed [X].
-
-
ECHO_TRUNCATED_TEXTFILE_NAMES =
-
1
<<~SHELL.strip
-
truncate_file() \
-
{ \
-
if [ $(stat -c%s "${1}") -gt #{MAX_FILE_SIZE} ]; then \
-
truncate -s #{MAX_FILE_SIZE+1} "${1}"; \
-
fi; \
-
}; \
-
is_text_file() \
-
{ \
-
if file --mime-encoding ${1} | grep -qv "${1}:\\sbinary"; then \
-
truncate_file "${1}"; \
-
true; \
-
elif [ $(stat -c%s "${1}") -lt 2 ]; then \
-
true; \
-
else \
-
false; \
-
fi; \
-
}; \
-
depathed() \
-
{ \
-
echo "${1:2}"; \
-
}; \
-
export -f truncate_file; \
-
export -f is_text_file; \
-
export -f depathed; \
-
(cd #{SANDBOX_DIR} && find . -type f -exec \
-
bash -c "is_text_file {} && depathed {}" \\;)
-
SHELL
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# container
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def create_container(image_name, id, max_seconds)
-
80
@container_name = ['cyber_dojo_runner', id, random_id].join('_')
-
docker_run = [
-
80
'docker run',
-
"--name=#{@container_name}",
-
docker_run_options(image_name, id),
-
image_name,
-
"bash -c 'sleep #{max_seconds}'"
-
].join(SPACE)
-
# The --detach run-option means this assert will not catch
-
# some errors. For example, if the container has no bash [X].
-
80
shell.assert(docker_run)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def remove_container
-
# Backgrounded for a small speed-up.
-
79
shell.exec("docker rm #{@container_name} --force &")
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def random_id
-
# Add a random-id to the container name. A container-name
-
# based on _only_ the id will fail when a container with
-
# that id exists and is alive. Note that remove_container()
-
# backgrounds the [docker rm].
-
80
HEX_DIGITS.shuffle[0,8].join
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
HEX_DIGITS = [*('a'..'z'),*('A'..'Z'),*('0'..'9')]
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def docker_run_options(image_name, id)
-
# [1] For clang/clang++'s -fsanitize=address
-
# [2] Makes container removal much faster
-
<<~SHELL.strip
-
80
#{env_vars(image_name, id)} \
-
#{TMP_FS_SANDBOX_DIR} \
-
#{TMP_FS_TMP_DIR} \
-
#{ulimits(image_name)} \
-
--cap-add=SYS_PTRACE `# [1]` \
-
--detach `# later docker execs` \
-
--init `# pid-1 process [2]` \
-
--rm `# auto rm on exit` \
-
--user=#{UID}:#{GID} `# not root`
-
SHELL
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def env_vars(image_name, id)
-
[
-
80
env_var('IMAGE_NAME', image_name),
-
env_var('ID', id),
-
env_var('SANDBOX', SANDBOX_DIR)
-
].join(SPACE)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def env_var(name, value)
-
# Note: value must not contain a single-quote
-
240
"--env CYBER_DOJO_#{name}='#{value}'"
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
TMP_FS_SANDBOX_DIR =
-
1
"--tmpfs #{SANDBOX_DIR}:" +
-
'exec,' + #Â [1]
-
'size=50M,' + # [2]
-
"uid=#{UID}," + # [3]
-
"gid=#{GID}" # [3]
-
# Making the sandbox dir a tmpfs should improve speed.
-
# By default, tmp-fs's are setup as secure mountpoints.
-
# If you use only '--tmpfs #{SANDBOX_DIR}'
-
# then a [cat /etc/mtab] will reveal something like
-
# "tmpfs /sandbox tmpfs rw,nosuid,nodev,noexec,relatime,size=10240k 0 0"
-
# o) rw = Mount the filesystem read-write.
-
# o) nosuid = Do not allow set-user-identifier or
-
# set-group-identifier bits to take effect.
-
# o) nodev = Do not interpret character or block special devices.
-
# o) noexec = Do not allow direct execution of any binaries.
-
# o) relatime = Update inode access times relative to modify/change time.
-
# So...
-
# [1] set exec to make binaries and scripts executable.
-
# [2] limit size of tmp-fs.
-
# [3] set ownership.
-
-
1
TMP_FS_TMP_DIR = '--tmpfs /tmp:exec,size=50M,mode=1777' # Set /tmp sticky-bit
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def ulimits(image_name)
-
# There is no cpu-ulimit... a cpu-ulimit of 10
-
# seconds could kill a container after only 5
-
# seconds... The cpu-ulimit assumes one core.
-
# The host system running the docker container
-
# can have multiple cores or use hyperthreading.
-
# So a piece of code running on 2 cores, both 100%
-
# utilized could be killed after 5 seconds.
-
options = [
-
80
ulimit('core' , 0 ), # core file size
-
ulimit('fsize' , 16*MB), # file size
-
ulimit('locks' , 128 ), # number of file locks
-
ulimit('nofile', 256 ), # number of files
-
ulimit('nproc' , 128 ), # number of processes
-
ulimit('stack' , 8*MB), # stack size
-
'--memory=512m', # max 512MB ram
-
'--net=none', # no network
-
'--pids-limit=128', # no fork bombs
-
'--security-opt=no-new-privileges', # no escalation
-
]
-
80
unless clang?(image_name)
-
# [ulimit data] prevents clang's -fsanitize=address option.
-
78
options << ulimit('data', 4*GB) # data segment size
-
end
-
80
options.join(SPACE)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def ulimit(name, limit)
-
558
"--ulimit #{name}=#{limit}"
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def clang?(image_name)
-
80
image_name.start_with?('cyberdojofoundation/clang')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# process helpers
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def Process_kill_group(pid)
-
# The [docker run] process running on the _host_ is
-
# killed by this Process.kill. This does _not_ kill the
-
# cyber-dojo.sh process running _inside_ the docker
-
# container. The container is killed by remove_container()
-
# with a fall-back via [docker run]'s --rm option.
-
3
Process.kill(-KILL_SIGNAL, pid) # -ve means kill process-group
-
rescue Errno::ESRCH
-
# There may no longer be a process at pid (timeout race).
-
# If not, you get an exception Errno::ESRCH: No such process
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def Process_detach(pid)
-
# Prevents zombie child-process. Don't wait for detach status.
-
3
Process.detach(pid)
-
# There may no longer be a process at pid (timeout race).
-
# If not, you don't get an exception.
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def killed?(status)
-
76
status === KILLED_STATUS
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
KILL_SIGNAL = 9
-
-
1
KILLED_STATUS = 128 + KILL_SIGNAL
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# file content helpers
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def packaged(raw_content)
-
755
content = Utf8.clean(raw_content)
-
{
-
755
'content' => truncated(content),
-
'truncated' => truncated?(content)
-
}
-
end
-
-
1
def truncated(content)
-
755
content[0...MAX_FILE_SIZE]
-
end
-
-
1
def truncated?(content)
-
755
content.size > MAX_FILE_SIZE
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
# externals
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def shell
-
238
@externals.shell
-
end
-
-
1
SPACE = ' '
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'shell_assert_error'
-
-
1
class Shell
-
-
1
def initialize(externals)
-
86
@externals = externals
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def exec(command)
-
162
bash_run(command)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert(command)
-
99
stdout,stderr,status = bash_run(command)
-
97
unless success?(status)
-
1
args = [command,stdout,stderr,status]
-
1
raise ShellAssertError.new(*args)
-
end
-
96
stdout
-
end
-
-
1
private
-
-
1
SUCCESS = 0
-
-
1
def success?(status)
-
355
status === SUCCESS
-
end
-
-
1
def bash_run(command)
-
261
stdout,stderr,status = bash.run(command)
-
258
unless success?(status) && ignore?(stderr)
-
11
args = [command,stdout,stderr,status]
-
11
log << ShellAssertError.new(*args).message
-
end
-
258
[stdout, stderr, status]
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def ignore?(stderr)
-
248
stderr.empty? || known_circle_ci_warning?(stderr)
-
end
-
-
KNOWN_CIRCLE_CI_WARNING =
-
1
"WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. " +
-
"Memory limited without swap."
-
-
1
def known_circle_ci_warning?(stderr)
-
2
on_circle_ci? && stderr.start_with?(KNOWN_CIRCLE_CI_WARNING)
-
end
-
-
1
def on_circle_ci?
-
2
ENV.include?('CIRCLECI')
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
def bash
-
261
@externals.bash
-
end
-
-
1
def log
-
11
@externals.log
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'utf8_clean'
-
1
require 'json'
-
-
1
class ShellAssertError < StandardError
-
-
1
def initialize(command, stdout, stderr, status)
-
15
super(JSON.pretty_generate({
-
'command' => Utf8.clean(command),
-
'stdout' => Utf8.clean(stdout),
-
'stderr' => Utf8.clean(stderr),
-
'status' => status
-
}))
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'rubygems/package' # Gem::Package::TarReader
-
1
require 'stringio'
-
-
1
module Tar
-
-
1
class Reader
-
-
1
def initialize(tar_file)
-
73
io = StringIO.new(tar_file, 'r+t')
-
73
@reader = Gem::Package::TarReader.new(io)
-
end
-
-
1
def files
-
# empty files are coming back as nil
-
673
@reader.map { |e| [e.full_name, e.read || ''] }.to_h
-
end
-
-
end
-
-
end
-
1
require 'rubygems/package' # Gem::Package::TarWriter
-
1
require 'stringio'
-
-
1
module Tar
-
-
1
class Writer
-
-
1
def initialize(files = {})
-
82
@tar_file = StringIO.new('')
-
82
@writer = Gem::Package::TarWriter.new(@tar_file)
-
82
files.each do |filename, content|
-
261
write(filename, content)
-
end
-
end
-
-
1
def write(filename, content)
-
265
size = content.bytesize
-
265
@writer.add_file_simple(filename, 0o644, size) do |fd|
-
265
fd.write(content)
-
end
-
end
-
-
1
def tar_file
-
81
@tar_file.string
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'empty'
-
-
1
module TrafficLight
-
-
1
def traffic_light(image_name, id)
-
79
if @result['run_cyber_dojo_sh'][:timed_out]
-
3
return
-
end
-
-
76
rag_src = @result['rag_src']
-
76
if rag_src.nil?
-
8
@result.merge!({ 'colour' => 'faulty' })
-
8
@result['diagnostic'] ||= {}
-
8
@result['diagnostic'].merge!({
-
'image_name' => image_name,
-
'id' => id,
-
'info' => "no /usr/local/bin/red_amber_green.rb in #{image_name}"
-
})
-
8
return
-
end
-
-
begin
-
68
rag_lambda = Empty.binding.eval(rag_src)
-
rescue Exception => error
-
3
@result.merge!({ 'colour' => 'faulty' })
-
3
@result['diagnostic'] ||= {}
-
3
@result['diagnostic'].merge!({
-
'image_name' => image_name,
-
'id' => id,
-
'info' => 'eval(rag_lambda) raised an exception',
-
'name' => error.class.name,
-
'message' => error.message.split("\n"),
-
'rag_lambda' => rag_src.split("\n")
-
})
-
3
return
-
end
-
-
begin
-
65
stdout = @result['run_cyber_dojo_sh'][:stdout]['content']
-
65
stderr = @result['run_cyber_dojo_sh'][:stderr]['content']
-
65
status = @result['run_cyber_dojo_sh'][:status]
-
65
colour = rag_lambda.call(stdout, stderr, status).to_s
-
rescue => error
-
3
@result.merge!({ 'colour' => 'faulty' })
-
3
@result['diagnostic'] ||= {}
-
3
@result['diagnostic'].merge!({
-
'image_name' => image_name,
-
'id' => id,
-
'info' => 'rag_lambda.call raised an exception',
-
'name' => error.class.name,
-
'message' => error.message.split("\n"),
-
'rag_lambda' => rag_src.split("\n")
-
})
-
3
return
-
end
-
-
62
unless colour === 'red' || colour === 'amber' || colour === 'green'
-
1
@result.merge!({ 'colour' => 'faulty' })
-
1
@result['diagnostic'] ||= {}
-
1
@result['diagnostic'].merge!({
-
'image_name' => image_name,
-
'id' => id,
-
'info' => "rag_lambda.call is '#{colour}' which is not 'red'|'amber'|'green'",
-
'rag_lambda' => rag_src.split("\n")
-
})
-
1
return
-
end
-
-
61
@result.merge!({ 'colour' => colour })
-
end
-
-
end
-
# frozen_string_literal: true
-
1
module Utf8
-
-
1
def self.clean(s)
-
# force an encoding change
-
# if encoding is already utf-8 then encoding
-
# to utf-8 is a no-op and invalid byte sequences
-
# are not detected.
-
801
s = s.encode('UTF-16', 'UTF-8', :invalid => :replace, :replace => '')
-
801
s = s.encode('UTF-8', 'UTF-16')
-
end
-
-
end
-
-
# http://robots.thoughtbot.com/fight-back-utf-8-invalid-byte-sequences
-
1
require 'minitest/autorun'
-
-
1
def require_src(required)
-
10
require_relative "../app/src/#{required}"
-
end
-
-
1
class Id58TestBase < MiniTest::Test
-
-
1
def initialize(arg)
-
112
@_id58 = nil
-
112
@_name58 = nil
-
112
super
-
end
-
-
1
@@args = (ARGV.sort.uniq - ['--']) # eg 2m4
-
1
@@seen_ids = []
-
1
@@timings = {}
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def self.define_test(os, display_name, id58_suffix, *lines, &test_block)
-
112
src = test_block.source_location
-
112
src_file = File.basename(src[0])
-
112
src_line = src[1].to_s
-
112
id58 = checked_id58(id58_suffix, lines)
-
112
if @@args === [] || @@args.any?{ |arg| id58.include?(arg) }
-
112
name58 = lines.join(space = ' ')
-
112
execute_around = lambda {
-
112
ENV['ID58'] = id58
-
112
@_os = os
-
112
@_display_name = display_name
-
112
@_id58 = id58
-
112
@_name58 = name58
-
112
id58_setup
-
begin
-
112
t1 = Time.now
-
112
self.instance_eval(&test_block)
-
112
t2 = Time.now
-
112
@@timings[id58+':'+src_file+':'+src_line+':'+name58] = (t2 - t1)
-
ensure
-
112
puts $!.message unless $!.nil?
-
112
id58_teardown
-
end
-
}
-
112
name = "id58 '#{id58_suffix}',\n'#{name58}'"
-
112
define_method("test_\n#{name}".to_sym, &execute_around)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
ObjectSpace.define_finalizer(self, proc {
-
skipped
slow = @@timings.select{ |_name,secs| secs > 0.000 }
-
skipped
sorted = slow.sort_by{ |name,secs| -secs }.to_h
-
skipped
size = sorted.size < 5 ? sorted.size : 5
-
skipped
puts
-
skipped
puts "Slowest #{size} tests are..." if size != 0
-
skipped
sorted.each_with_index { |(name,secs),index|
-
skipped
puts "%3.4f - %-72s" % [secs,name]
-
skipped
break if index === size
-
skipped
}
-
skipped
puts
-
skipped
})
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
ID58_ALPHABET = %w{
-
1
0 1 2 3 4 5 6 7 8 9
-
A B C D E F G H J K L M N P Q R S T U V W X Y Z
-
a b c d e f g h j k l m n p q r s t u v w x y z
-
}.join.freeze
-
-
1
def self.id58?(s)
-
224
s.is_a?(String) &&
-
716
s.chars.all?{ |ch| ID58_ALPHABET.include?(ch) }
-
end
-
-
1
def self.checked_id58(id58_suffix, lines)
-
112
method = 'def self.id58_prefix'
-
112
pointer = ' ' * method.index('.') + '!'
-
112
pointee = (['',pointer,method,'','']).join("\n")
-
112
pointer.prepend("\n\n")
-
112
raise "#{pointer}missing#{pointee}" unless respond_to?(:id58_prefix)
-
112
raise "#{pointer}empty#{pointee}" if id58_prefix === ''
-
112
raise "#{pointer}not id58#{pointee}" unless id58?(id58_prefix)
-
-
112
method = "test '#{id58_suffix}',"
-
112
pointer = ' ' * method.index("'") + '!'
-
112
proposition = lines.join(space = ' ')
-
112
pointee = ['',pointer,method,"'#{proposition}'",'',''].join("\n")
-
112
id58 = id58_prefix + id58_suffix
-
112
pointer.prepend("\n\n")
-
112
raise "#{pointer}empty#{pointee}" if id58_suffix === ''
-
112
raise "#{pointer}not id58#{pointee}" unless id58?(id58_suffix)
-
112
raise "#{pointer}duplicate#{pointee}" if @@seen_ids.include?(id58)
-
112
raise "#{pointer}overlap#{pointee}" if id58_prefix[-2..-1] === id58_suffix[0..1]
-
112
@@seen_ids << id58
-
112
id58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def id58_setup
-
end
-
-
1
def id58_teardown
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def os
-
6
@_os
-
end
-
-
1
def display_name
-
68
@_display_name
-
end
-
-
1
def id58
-
105
@_id58
-
end
-
-
skipped
# :nocov:
-
skipped
def name58
-
skipped
@_name58
-
skipped
end
-
skipped
# :nocov:
-
-
end
-
-
1
class BashStub
-
-
1
def initialize
-
6
test_id = ENV['ID58']
-
6
@filename = Dir.tmpdir + '/cyber_dojo_bash_stub_' + test_id + '.json'
-
6
unless File.file?(filename)
-
6
write([])
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def teardown
-
4
unless uncaught_exception?
-
3
stubs = read
-
3
pretty = JSON.pretty_generate(stubs)
-
3
unless stubs === []
-
1
raise "#{filename}: uncalled stubs(#{pretty})"
-
end
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def stub_run(command, stdout, stderr, status)
-
4
stubs = read
-
stub = {
-
4
command:command,
-
stdout:stdout,
-
stderr:stderr,
-
status:status
-
}
-
4
write(stubs << stub)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def run(command)
-
3
stubs = read
-
3
stub = stubs.shift
-
3
write(stubs)
-
3
if stub.nil?
-
1
raise [
-
self.class.name,
-
"run(command) - no stub",
-
"actual-command: #{command}",
-
].join("\n") + "\n"
-
end
-
2
unless command === stub['command']
-
1
raise [
-
self.class.name,
-
"run(command) - does not match stub",
-
"actual-command: #{command}",
-
"stubbed-command: #{stub['command']}"
-
].join("\n") + "\n"
-
end
-
1
[stub['stdout'], stub['stderr'], stub['status']]
-
end
-
-
1
private # = = = = = = = = = = = = = = = = = =
-
-
1
def read
-
10
JSON.parse(IO.read(filename))
-
end
-
-
1
def write(stubs)
-
13
IO.write(filename, JSON.unparse(stubs))
-
end
-
-
1
def filename
-
30
@filename
-
end
-
-
1
def uncaught_exception?
-
4
$!
-
end
-
-
end
-
-
1
class BashStubRaiser
-
-
1
def initialize(message)
-
1
@message = message
-
1
@fired_count = 0
-
end
-
-
1
def fired_once?
-
1
@fired_count === 1
-
end
-
-
1
def run(command)
-
1
@fired_count += 1
-
1
raise ArgumentError.new(@message)
-
end
-
-
end
-
1
require 'open3'
-
-
1
class BashStubTarPipeOut
-
-
1
def initialize(content)
-
3
@content = content
-
3
@fired_count = 0
-
end
-
-
1
def fired_once?
-
3
@fired_count === 1
-
end
-
-
1
def run(command)
-
9
if command.include?('is_text_file')
-
3
@fired_count += 1
-
3
return stdout=@content,stderr='',status=1
-
else
-
6
stdout,stderr,r = Open3.capture3(command)
-
6
[ stdout, stderr, r.exitstatus ]
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_relative 'bash_stub'
-
-
1
class BashStubTest < TestBase
-
-
1
def self.id58_prefix
-
24
'F03'
-
end
-
-
1
def id58_setup
-
6
@bash = BashStub.new
-
end
-
-
1
attr_reader :bash
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '4A5',
-
%w( teardown does not raise
-
when no run()s are stubbed
-
and no run()s are made
-
) do
-
1
bash.teardown
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '652',
-
%w( run() raises when run() is not stubbed
-
) do
-
2
assert_raises { bash.run(pwd) }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '181',
-
%w( run() raises when run() is stubbed but for a different command
-
) do
-
1
bash.stub_run(pwd, wd, stderr='', success)
-
2
assert_raises { bash.run(not_pwd = "cd #{wd}") }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test 'B4E',
-
%w( teardown does not raise
-
when one run() is stubbed
-
and a matching run() is made
-
) do
-
1
bash.stub_run(pwd, wd, stderr='', success)
-
1
stdout,stderr,status = bash.run('pwd')
-
1
assert_equal wd, stdout
-
1
assert_equal '', stderr
-
1
assert_equal success, status
-
1
bash.teardown
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test 'D0C',
-
%w( teardown raises
-
when a run() is stubbed
-
and no run() is made
-
) do
-
1
bash.stub_run(pwd, wd, stderr='', success)
-
2
assert_raises { bash.teardown }
-
end
-
-
# - - - - - - - - - - - - - - -
-
-
1
test '470',
-
%w( teardown does not raise
-
when there is an uncaught exception
-
) do
-
1
bash.stub_run(pwd, wd, stderr='', success)
-
1
error = assert_raises {
-
begin
-
1
raise 'forced'
-
ensure
-
1
bash.teardown
-
end
-
}
-
1
assert_equal 'forced', error.message
-
end
-
-
1
private # = = = = = = = = = = = = = = =
-
-
1
def pwd
-
5
'pwd'
-
end
-
-
1
def wd
-
6
'/Users/jonjagger/repos/web'
-
end
-
-
1
def success
-
5
0
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class AliveTest < TestBase
-
-
1
def self.id58_prefix
-
4
'6de'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '190', %w(
-
alive? is true, useful for k8s liveness probes
-
) do
-
1
assert alive?
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class BaselineSpeedTest < TestBase
-
-
1
def self.id58_prefix
-
8
'159'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '8A6',
-
'baseline average speed is less than 1.7 secs' do
-
2
timings = []
-
2
(1..5).each do
-
10
started_at = Time.now
-
10
assert_cyber_dojo_sh('true')
-
10
stopped_at = Time.now
-
10
diff = Time.at(stopped_at - started_at).utc
-
10
secs = diff.strftime("%S").to_i
-
10
millisecs = diff.strftime("%L").to_i
-
10
timings << (secs * 1000 + millisecs)
-
end
-
2
mean = timings.reduce(0, :+) / timings.size
-
2
assert mean < max=1700, "mean=#{mean}, max=#{max}"
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class BombRobustNessTest < TestBase
-
-
1
def self.id58_prefix
-
24
'1B5'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test 'CD5',
-
'fork-bomb does not run indefinitely' do
-
1
with_captured_log {
-
1
run_cyber_dojo_sh({
-
changed: { 'hiker.c' => C_FORK_BOMB },
-
max_seconds: 3
-
})
-
}
-
1
assert timed_out? ||
-
printed?('fork()') ||
-
daemon_error? ||
-
no_such_container_error?, result
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test 'CD6',
-
'shell fork-bomb does not run indefinitely' do
-
2
with_captured_log {
-
2
run_cyber_dojo_sh({
-
changed: { 'cyber-dojo.sh' => SHELL_FORK_BOMB },
-
max_seconds: 3
-
})
-
}
-
-
2
cant_fork = (os === :Alpine ? "can't fork" : 'Cannot fork')
-
assert \
-
2
timed_out? ||
-
printed?(cant_fork) ||
-
printed?('bomb') ||
-
daemon_error? ||
-
no_such_container_error?, result
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test 'DB3',
-
'file-handles quickly become exhausted' do
-
1
with_captured_log {
-
1
run_cyber_dojo_sh({
-
changed: { 'hiker.c' => FILE_HANDLE_BOMB },
-
max_seconds: 3
-
})
-
}
-
1
assert printed?('fopen() != NULL') ||
-
daemon_error? ||
-
no_such_container_error?, result
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '62B',
-
%w( a crippled container, eg from a fork-bomb, returns everything unchanged ) do
-
2
stub = BashStubTarPipeOut.new('fail')
-
2
@externals = Externals.new({ 'bash' => stub })
-
4
with_captured_log { run_cyber_dojo_sh }
-
2
assert stub.fired_once?
-
2
assert_created({})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
1
private # = = = = = = = = = = = = = = = = = = = = = =
-
-
1
C_FORK_BOMB = <<~'CODE'
-
#include "hiker.h"
-
#include <stdio.h>
-
#include <unistd.h>
-
int answer(void)
-
{
-
for(;;)
-
{
-
int pid = fork();
-
fprintf(stdout, "fork() => %d\n", pid);
-
fflush(stdout);
-
if (pid == -1)
-
break;
-
}
-
return 6 * 7;
-
}
-
CODE
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
SHELL_FORK_BOMB = <<~CODE
-
bomb()
-
{
-
echo "bomb"
-
bomb | bomb &
-
}
-
bomb
-
CODE
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
FILE_HANDLE_BOMB = <<~'CODE'
-
#include "hiker.h"
-
#include <stdio.h>
-
int answer(void)
-
{
-
for (int i = 0;;i++)
-
{
-
char filename[42];
-
sprintf(filename, "wibble%d.txt", i);
-
FILE * f = fopen(filename, "w");
-
if (f)
-
fprintf(stdout, "fopen() != NULL %s\n", filename);
-
else
-
{
-
fprintf(stdout, "fopen() == NULL %s\n", filename);
-
break;
-
}
-
}
-
return 6 * 7;
-
}
-
CODE
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
def printed?(text)
-
skipped
(stdout+stderr).lines.any? { |line| line.include?(text) }
-
skipped
end
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
def daemon_error?
-
skipped
printed?('Error response from daemon: No such container') ||
-
skipped
regexp?(/Error response from daemon: Container .* is not running/)
-
skipped
end
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
def regexp?(pattern)
-
skipped
(stdout+stderr) =~ pattern
-
skipped
end
-
skipped
# :nocov:
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
skipped
# :nocov:
-
skipped
def no_such_container_error?
-
skipped
stderr.start_with?('Error: No such container: cyber_dojo_runner_')
-
skipped
end
-
skipped
# :nocov:
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ClangSanitizeAddressTest < TestBase
-
-
1
def self.id58_prefix
-
4
'D28'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
clang_assert_test '0BB',
-
%w( clang sanitize address ) do
-
1
diagnostic = 'AddressSanitizer: heap-use-after-free on address'
-
1
run_cyber_dojo_sh
-
1
refute stderr.include?(diagnostic), stderr
-
1
run_cyber_dojo_sh( {
-
changed:{ 'hiker.c' => leaks_memory }
-
})
-
1
assert stderr.include?(diagnostic), stderr
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def leaks_memory
-
1
<<~C_SOURCE
-
#include "hiker.h"
-
#include <stdlib.h>
-
-
int answer(void)
-
{
-
int * p = (int *)malloc(64);
-
p[0] = 6;
-
free(p);
-
return p[0] * 7;
-
}
-
C_SOURCE
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ContainerPropertiesTest < TestBase
-
-
1
def self.id58_prefix
-
12
'3A8'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'D91', %w(
-
requires bash, won't run in sh ) do
-
1
assert_equal '/bin/bash', assert_cyber_dojo_sh('printf ${SHELL}')
-
1
image_name = 'alpine:latest' # has sh but not bash
-
1
with_captured_log {
-
1
run_cyber_dojo_sh({image_name:image_name})
-
}
-
-
# main command is [docker run --detach IMAGE bash -c 'sleep 10']
-
# The --detach means lack of bash is not a [docker run] error.
-
# Subsequent failure behavior is dependent on non determinstic timings.
-
1
assert stdout === '' || stdout.start_with?('cannot exec in a stopped state:'), ":#{stdout}:"
-
1
assert stderr === '' || stderr.start_with?('Error response from daemon:'), ":#{stderr}:"
-
-
1
expected_info = "no /usr/local/bin/red_amber_green.rb in #{image_name}"
-
1
assert_equal 'faulty', colour
-
1
assert_equal image_name, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_nil diagnostic['message'], :message
-
1
assert_nil diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test 'D98', %w( multiple container properties ) do
-
cyber_dojo_sh = [
-
2
"#{stat_cmd} > #{sandbox_dir}/files.stat", #Â [1]
-
"cat /proc/1/cmdline | cut -c1-9 > #{sandbox_dir}/proc.1", # [2]
-
"cat /etc/passwd > #{sandbox_dir}/passwd",
-
"getent group #{group} > #{sandbox_dir}/group",
-
"printf ${HOME} > #{sandbox_dir}/home.dir",
-
"env > #{sandbox_dir}/env.vars",
-
"stat --printf='%u' #{sandbox_dir} > #{sandbox_dir}/dir.stat.u",
-
"stat --printf='%g' #{sandbox_dir} > #{sandbox_dir}/dir.stat.g",
-
"stat --printf='%A' #{sandbox_dir} > #{sandbox_dir}/dir.stat.A",
-
"ulimit -a > #{sandbox_dir}/ulimit.all"
-
].join(' && ')
-
-
2
assert_cyber_dojo_sh(cyber_dojo_sh)
-
-
# [1] must be first so as not to see newly created files.
-
# [2] On CircleCI, currently proc.1 is... '/dev/init' + 0.chr + '--'
-
# Yes, there is an embedded nul-character.
-
# Depending on the version of docker you are using you may get
-
# '/sbin/docker-init' instead of '/dev/init'
-
# Either way, the embedded nul-character causes text_file_changes()
-
# in runner.rb to see proc.1 as a binary file. Hence only the first
-
# nine characters of proc/1/cmdline are saved, and proc.1 is seen
-
# as a text file.
-
2
proc1 = created_file('proc.1')
-
2
expected_1 = '/dev/init'
-
2
expected_2 = '/sbin/docker-init'[0...expected_1.size]
-
6
assert [expected_1,expected_2].any?{|s|proc1.start_with?(s)}, proc1
-
-
2
etc_passwd = created_file('passwd')
-
2
etc_passwd_line = "sandbox:x:#{uid}:#{gid}:"
-
50
assert etc_passwd.lines.detect{|line| line.start_with?(etc_passwd_line)}, etc_passwd
-
-
2
fields = created_file('group').split(':') # sandbox:x:51966
-
2
assert_equal group, fields[0], :group_name
-
2
assert_equal gid, fields[2].to_i, :group_gid
-
-
2
assert_equal home_dir, created_file('home.dir'), :home_dir
-
-
2
env = created_file('env.vars')
-
23
env_vars = Hash[env.split("\n").map{ |line| line.split('=') }]
-
2
assert_equal image_name, env_vars['CYBER_DOJO_IMAGE_NAME'], :cyber_dojo_image_name
-
2
assert_equal id, env_vars['CYBER_DOJO_ID'], :cyber_dojo_id
-
2
assert_equal sandbox_dir, env_vars['CYBER_DOJO_SANDBOX'], :cyber_dojo_sandbox
-
-
2
assert_equal uid.to_s, created_file('dir.stat.u'), :uid
-
2
assert_equal gid.to_s, created_file('dir.stat.g'), :gid
-
2
assert_equal 'drwxrwxrwt', created_file('dir.stat.A'), :permission
-
-
2
expected_max_data_size = clang? ? 0 : 4 * GB / KB
-
2
expected_max_file_size = 16 * MB / (block_size = 1024)
-
2
expected_max_stack_size = 8 * MB / KB
-
2
assert_equal expected_max_data_size, ulimit(:data_size), :data_size
-
2
assert_equal expected_max_file_size, ulimit(:file_size), :file_size
-
2
assert_equal expected_max_stack_size, ulimit(:stack_size), :stack_size
-
2
assert_equal 0, ulimit(:core_size), :core_size
-
2
assert_equal 128, ulimit(:file_locks), :file_locks
-
2
assert_equal 256, ulimit(:open_files), :open_files
-
2
assert_equal 128, ulimit(:processes), :processes
-
-
2
stats = files_stat
-
2
assert_equal starting_files.keys.sort, stats.keys.sort
-
2
starting_files.each do |filename, content|
-
6
if filename === 'cyber-dojo.sh'
-
2
content = cyber_dojo_sh
-
end
-
6
stat = stats[filename]
-
6
refute_nil stat, filename
-
6
diagnostic = { filename => stat }
-
6
assert_equal '-rw-r--r--', stat[:permissions], diagnostic
-
6
assert_equal uid, stat[:uid ], diagnostic
-
6
assert_equal group, stat[:group ], diagnostic
-
6
assert_equal content.size, stat[:size ], diagnostic
-
# On _default_ Alpine date-time file-stamps are to
-
# the second granularity. In other words, the
-
# microseconds value is always '000000000'.
-
# Make sure this had been upgraded.
-
6
stamp = stat[:time] # eg '07:03:14.835233538'
-
6
microsecs = stamp.split(/[\:\.]/)[-1]
-
6
assert_equal 9, microsecs.length, :microsecs_length
-
6
refute_equal '0'*9, microsecs, :microsecs_not_zero
-
end
-
end
-
-
1
private
-
-
1
def home_dir
-
2
'/home/sandbox'
-
end
-
-
1
def sandbox_dir
-
28
'/sandbox'
-
end
-
-
1
def gid
-
6
51966
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
KB = 1024
-
1
MB = 1024 * KB
-
1
GB = 1024 * MB
-
-
1
def clang?
-
2
image_name.start_with?('cyberdojofoundation/clang')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
ULIMIT_TABLE = {
-
1
:core_size => 'core file size',
-
:data_size => 'data seg size',
-
:file_locks => 'file locks',
-
:file_size => 'file size',
-
:open_files => 'open files',
-
:processes => 'max user processes',
-
:stack_size => 'stack size'
-
}
-
-
1
def ulimit(key)
-
14
ulimit = created_file('ulimit.all')
-
14
text = ULIMIT_TABLE[key]
-
14
diagnostic = "#{ulimit}\nno ulimit for #{key}"
-
14
refute_nil text, diagnostic
-
128
entry = ulimit.lines.find { |line| line.start_with?(text) }
-
14
entry.split[-1].to_i
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def files_stat
-
2
created_file('files.stat').lines.collect { |line|
-
6
attr = line.split
-
6
[attr[0], { # filename
-
permissions: attr[1],
-
uid: attr[2].to_i,
-
group: attr[3],
-
size: attr[4].to_i, # [5] === date
-
time: attr[6],
-
}]
-
}.to_h
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def created_file(filename)
-
32
created[filename]['content']
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class LargeFileTruncationTest < TestBase
-
-
1
def self.id58_prefix
-
12
'E4A'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '52A',
-
%w( generated text files bigger than 50K are truncated ) do
-
2
filename = 'large_file.txt'
-
2
script = "od -An -x /dev/urandom | head -c#{51*1024} > #{filename}"
-
2
script += ";stat -c%s #{filename}"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_equal "#{51*1024}\n", stdout, :stdout
-
2
assert created[filename]['truncated'], :truncated
-
2
assert_equal 50*1024, created[filename]['content'].size, :size
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '52B',
-
%w( source files bigger than 10K are not truncated ) do
-
1
filename = 'Hiker.cs'
-
1
src = starting_files[filename]
-
1
large_comment = "/*#{'x'*10*1024}*/"
-
1
refute_nil src
-
1
run_cyber_dojo_sh( {
-
changed:{
-
filename => src + large_comment
-
}
-
})
-
1
refute changed.keys.include?(filename)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ReadyTest < TestBase
-
-
1
def self.id58_prefix
-
4
'872'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '190', %w(
-
ready? is true, useful for k8s readyness probes
-
) do
-
1
assert ready?
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ReturnedTextFilesTest < TestBase
-
-
1
def self.id58_prefix
-
72
'ECF'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '524', %w(
-
created text-file filenames are returned even when
-
their names have leading hyphens which could easily
-
be read as a tar-pipe option
-
) do
-
2
leading_hyphen = '-JPlOLNY7yt_fFndapHwIg'
-
2
script = "printf 'xxx' > '#{leading_hyphen}';"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({leading_hyphen => intact('xxx')})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '526', %w(
-
created text files, including dot files, are returned
-
) do
-
2
script = [
-
'printf "xxx" > newfile.txt',
-
'printf "yyy" > .dotfile'
-
].join(';')
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({
-
'newfile.txt' => intact('xxx'),
-
'.dotfile' => intact('yyy')
-
})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '527', %w(
-
created binary files are not returned
-
) do
-
2
script = [
-
'dd if=/dev/zero of=binary.dat bs=1c count=42',
-
'file --mime-encoding binary.dat'
-
].join(';')
-
2
assert_cyber_dojo_sh(script)
-
2
assert stdout.include?('binary.dat: binary')
-
2
assert_created({})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '529', %w(
-
text files created in sub-dirs are returned
-
) do
-
2
dirname = 'sub'
-
2
path = "#{dirname}/newfile.txt"
-
2
content = 'jjj'
-
script = [
-
2
"mkdir #{dirname}",
-
"printf '#{content}' > #{path}"
-
].join(';')
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({ path => intact(content) })
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '530', %w(
-
deleted files are detected
-
) do
-
2
filename = any_src_file
-
2
script = "rm #{filename}"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({})
-
2
assert_deleted([filename])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '531', %w(
-
changed files are detected
-
) do
-
2
filename = any_src_file
-
2
content = 'XXX'
-
2
script = "printf '#{content}' > #{filename}"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({})
-
2
assert_deleted([])
-
2
assert_changed({filename => intact(content)})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '532', %w(
-
empty new text files are detected
-
) do
-
# The file utility says empty files are binary files!
-
2
filename = 'empty.txt'
-
2
script = "touch #{filename}"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({filename => intact('')})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '533', %w(
-
single-char new text files are detected
-
) do
-
# The file utility says single-char files are binary files!
-
2
filename = 'one-char.txt'
-
2
ch = 'x'
-
2
script = "printf '#{ch}' > #{filename}"
-
2
assert_cyber_dojo_sh(script)
-
2
assert_created({filename => intact(ch)})
-
2
assert_deleted([])
-
2
assert_changed({})
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '62C', %w(
-
no text files under /sandbox at all, returns everything deleted
-
) do
-
2
assert_cyber_dojo_sh('rm -rf /sandbox/* /sandbox/.*')
-
2
assert_created({})
-
2
assert_deleted(manifest['visible_files'].keys.sort)
-
2
assert_changed({})
-
end
-
-
1
private # = = = = = = = = = = = = =
-
-
1
def any_src_file
-
4
manifest['visible_files'].keys.find do |filename|
-
8
filename.split('.')[0].upcase === 'HIKER'
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class SandboxSubDirTest < TestBase
-
-
1
def self.id58_prefix
-
24
'D8D'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '12A',
-
'browser can create files in sandbox/ sub-dirs' do
-
# The tar-pipe handles creating dir structure
-
2
assert_browser_can_create_files_in_sandbox_sub_dir('s1')
-
2
assert_browser_can_create_files_in_sandbox_sub_dir('s1/s2')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '12B',
-
'cyber-dojo.sh can create files in sandbox/ sub-dirs' do
-
2
assert_cyber_dojo_sh_can_create_files_in_sandbox_sub_dir('d1')
-
2
assert_cyber_dojo_sh_can_create_files_in_sandbox_sub_dir('d1/d2/d3')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '12C',
-
%w( cyber-dojo.sh can delete files from sandbox/ sub-dir ) do
-
2
assert_cyber_dojo_sh_can_delete_files_from_sandbox_sub_dir('c1')
-
2
assert_cyber_dojo_sh_can_delete_files_from_sandbox_sub_dir('c1/c2/c3')
-
end
-
-
1
private # = = = = = = = = = = = = = = = = = = = = = =
-
-
1
def assert_browser_can_create_files_in_sandbox_sub_dir(sub_dir)
-
4
filename = 'hello.txt'
-
4
content = 'the boy stood on the burning deck'
-
4
run_cyber_dojo_sh({
-
created: { "#{sub_dir}/#{filename}" => content },
-
changed: { 'cyber-dojo.sh' => "cd #{sub_dir} && #{stat_cmd}" }
-
})
-
4
assert_stats(filename, '-rw-r--r--', content.length)
-
4
assert_equal({}, created, :created)
-
4
assert_equal({}, changed, :changed)
-
4
assert_equal([], deleted, :deleted)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_cyber_dojo_sh_can_create_files_in_sandbox_sub_dir(sub_dir)
-
4
filename = 'bonjour.txt'
-
4
content = 'xyzzy'
-
cmd = [
-
4
"mkdir -p #{sub_dir}",
-
"printf #{content} > #{sub_dir}/#{filename}",
-
"cd #{sub_dir}",
-
stat_cmd
-
].join(' && ')
-
4
run_cyber_dojo_sh({
-
changed: { 'cyber-dojo.sh' => cmd }
-
})
-
4
assert_stats(filename, '-rw-r--r--', content.length)
-
expected = {
-
4
"#{sub_dir}/#{filename}" => {
-
'content' => content,
-
'truncated' => false
-
}
-
}
-
4
assert_equal(expected, created, :created)
-
4
assert_equal({}, changed, :changed)
-
4
assert_equal([], deleted, :deleted)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_cyber_dojo_sh_can_delete_files_from_sandbox_sub_dir(sub_dir)
-
4
filename = "#{sub_dir}/goodbye.txt"
-
4
content = 'goodbye, world'
-
cmd = [
-
4
"rm #{filename}",
-
"cd #{sub_dir}",
-
stat_cmd
-
].join(' && ')
-
4
run_cyber_dojo_sh({
-
created: { filename => content },
-
changed: { 'cyber-dojo.sh' => cmd }
-
})
-
4
assert_equal([], stdout_stats.keys, :keys)
-
4
assert_equal({}, created, :created)
-
4
assert_equal({}, changed, :changed)
-
4
assert_equal([filename], deleted, :deleted)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_stats(filename, permissions, size)
-
8
stats = stdout_stats[filename]
-
8
refute_nil stats, filename
-
8
diagnostic = { filename => stats }
-
8
assert_equal permissions, stats[:permissions], diagnostic
-
8
assert_equal uid, stats[:uid ], diagnostic
-
8
assert_equal group, stats[:group], diagnostic
-
8
assert_equal size, stats[:size ], diagnostic
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def stdout_stats
-
12
stdout.lines.collect { |line|
-
8
attr = line.split
-
8
[attr[0], { # filename eg hiker.h
-
permissions: attr[1], # eg -rwxr--r--
-
uid: attr[2].to_i, # eg 40045
-
group: attr[3], # eg cyber-dojo
-
size: attr[4].to_i, # eg 136
-
time_stamp: attr[6], # eg 07:03:14.539952547
-
}]
-
}.to_h
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ShaTest < TestBase
-
-
1
def self.id58_prefix
-
4
'FB3'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '190', %w(
-
sha of git commit which created docker image is available through API
-
) do
-
1
assert_sha(sha)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class TimedOutTest < TestBase
-
-
1
def self.id58_prefix
-
12
'9E9'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'B2A', %w(
-
when timed_out is false,
-
then the traffic-light colour is set
-
) do
-
1
run_cyber_dojo_sh
-
1
refute_timed_out
-
1
assert_equal 'red', colour, :colour
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test 'B2B', %w(
-
when run_cyber_dojo_sh does not complete within max_seconds
-
and does not produce output
-
then stdout is empty,
-
and timed_out is true,
-
and the traffic-light colour is not set
-
) do
-
named_args = {
-
1
changed: { 'hiker.c' => quiet_infinite_loop },
-
max_seconds: 2
-
}
-
1
with_captured_log {
-
1
run_cyber_dojo_sh(named_args)
-
}
-
1
assert_timed_out
-
1
assert_equal '', stdout, :stdout
-
1
assert_equal '', stderr, :stderr
-
1
refute colour?, :colour?
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test '4D7', %w(
-
when run_cyber_dojo_sh does not complete in max_seconds
-
and produces output
-
then stdout is not empty,
-
and timed_out is true,
-
and the traffic-light colour is not set
-
) do
-
named_args = {
-
1
changed: { 'hiker.c' => loud_infinite_loop },
-
max_seconds: 2
-
}
-
1
with_captured_log {
-
1
run_cyber_dojo_sh(named_args)
-
}
-
1
assert_timed_out
-
1
refute_equal '', stdout, :stdout
-
1
refute colour?, :colour?
-
end
-
-
1
private
-
-
1
def colour?
-
2
result.has_key?('colour')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def quiet_infinite_loop
-
1
<<~SOURCE
-
#include "hiker.h"
-
int answer(void)
-
{
-
for(;;);
-
return 6 * 7;
-
}
-
SOURCE
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def loud_infinite_loop
-
1
<<~SOURCE
-
#include "hiker.h"
-
#include <stdio.h>
-
int answer(void)
-
{
-
for(;;)
-
puts("Hello");
-
return 6 * 7;
-
}
-
SOURCE
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require 'tmpdir'
-
-
1
class TrafficLightTest < TestBase
-
-
1
def self.id58_prefix
-
68
'7B7'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '9DA', %w( stdout is not being whitespace stripped ) do
-
2
stdout = assert_cyber_dojo_sh('printf " hello \n"')
-
2
assert_equal " hello \n", stdout
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '9DB', %w( red traffic-light, no diagnostics ) do
-
2
run_cyber_dojo_sh
-
2
assert_equal 'red', colour, :colour
-
2
assert_nil diagnostic, :diagnostic
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '9DC', %w( amber traffic-light, no diagnostics ) do
-
2
syntax_error = starting_files[filename_6x9].sub('6 * 9', '6 * 9sdf')
-
2
run_cyber_dojo_sh({changed:{filename_6x9=>syntax_error}})
-
2
assert_equal 'amber', colour, :colour
-
2
assert_nil diagnostic, :diagnostic
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '9DD', %w( green traffic-light, no diagnostics ) do
-
2
passing = starting_files[filename_6x9].sub('6 * 9', '6 * 7')
-
2
run_cyber_dojo_sh({changed:{filename_6x9=>passing}})
-
2
assert_equal 'green', colour, :colour
-
2
assert_nil diagnostic, :diagnostic
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '421', %w(
-
when rag-lambda is missing,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
1
image_stub = "runner_test_stub:#{id}"
-
1
Dir.mktmpdir do |dir|
-
dockerfile = [
-
1
"FROM #{image_name}",
-
'RUN rm /usr/local/bin/red_amber_green.rb'
-
].join("\n")
-
1
IO.write("#{dir}/Dockerfile", dockerfile)
-
1
shell.assert("docker build --tag #{image_stub} #{dir}")
-
end
-
begin
-
1
run_cyber_dojo_sh({image_name:image_stub})
-
1
expected_info = "no /usr/local/bin/red_amber_green.rb in #{image_stub}"
-
1
assert_equal 'faulty', colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_nil diagnostic['name'], :name
-
1
assert_nil diagnostic['message'], :message
-
1
assert_nil diagnostic['rag_lambda'], :rag_lambda
-
ensure
-
1
shell.assert("docker image rm #{image_stub}")
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '422', %w(
-
when rag-lambda has an eval exception,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
stub =
-
1
<<~RUBY
-
sdf
-
RUBY
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'eval(rag_lambda) raised an exception'
-
1
expected_message = "undefined local variable or method `sdf'"
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'NameError', diagnostic['name'], :name
-
1
assert diagnostic['message'][0].start_with?(expected_message), :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '423', %w(
-
when rag-lambda has a call exception,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
stub =
-
1
<<~RUBY
-
lambda { |stdout, stderr, status|
-
raise ArgumentError.new('wibble')
-
}
-
RUBY
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'rag_lambda.call raised an exception'
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'ArgumentError', diagnostic['name'], :name
-
1
assert_equal ['wibble'], diagnostic['message'], :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '424', %w(
-
when the rag-lambda returns non red/amber/green,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
stub =
-
1
<<~RUBY
-
lambda { |stdout, stderr, status|
-
return :orange
-
}
-
RUBY
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = "rag_lambda.call is 'orange' which is not 'red'|'amber'|'green'"
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_nil diagnostic['name'], :name
-
1
assert_nil diagnostic['message'], :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '425', %w(
-
when the rag-lambda has too few parameters,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
stub =
-
1
<<~RUBY
-
lambda { |stdout, stderr|
-
return :red
-
}
-
RUBY
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'rag_lambda.call raised an exception'
-
1
expected_message = 'wrong number of arguments (given 3, expected 2)'
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'ArgumentError', diagnostic['name'], :name
-
1
assert_equal [expected_message], diagnostic['message'], :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '426', %w(
-
when the rag-lambda has too many parameters,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
stub =
-
1
<<~RUBY
-
lambda { |stdout, stderr, status, extra|
-
return :red
-
}
-
RUBY
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'rag_lambda.call raised an exception'
-
1
expected_message = 'wrong number of arguments (given 3, expected 4)'
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'ArgumentError', diagnostic['name'], :name
-
1
assert_equal [expected_message], diagnostic['message'], :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '427', %w(
-
when the rag-lambda has an eval SyntaxError exception,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
1
stub = 'return :red adsd'
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'eval(rag_lambda) raised an exception'
-
1
expected_message = 'syntax error, unexpected tIDENTIFIER'
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'SyntaxError', diagnostic['name'], :name
-
1
assert diagnostic['message'][0].include?(expected_message), :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '428', %w(
-
when the rag-lambda raises an Exception directly,
-
then the colour is faulty,
-
and diagnostics are added to the json result
-
) do
-
1
stub = 'raise Exception, "fubar"'
-
1
image_stub = run_cyber_dojo_image_stubbed_with(stub)
-
1
expected_info = 'eval(rag_lambda) raised an exception'
-
1
assert_equal 'faulty', colour, :colour
-
1
assert_equal image_stub, diagnostic['image_name'], :image_name
-
1
assert_equal id, diagnostic['id'], :id
-
1
assert_equal expected_info, diagnostic['info'], :info
-
1
assert_equal 'Exception', diagnostic['name'], :name
-
1
assert_equal ['fubar'], diagnostic['message'], :message
-
1
assert_equal stub.split("\n"), diagnostic['rag_lambda'], :rag_lambda
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '429', %w(
-
when the base-os is Ubuntu 16.04,
-
then the colour is faulty,
-
and diagnostics are added to the json result,
-
stating the file command was not found,
-
stating --verbatim-files-from is an unrecognized tar option
-
) do
-
1
log = with_captured_log {
-
1
run_cyber_dojo_sh({image_name:'ubuntu:16.04'})
-
}
-
1
assert_equal 'faulty', colour, :colour
-
1
no_file_msg = 'bash: file: command not found'
-
1
no_tar_verbatim_msg = "tar: unrecognized option '--verbatim-files-from'"
-
1
assert log.include?(no_file_msg), log
-
1
assert diagnostic['stderr'].include?(no_file_msg), diagnostic
-
1
assert log.include?(no_tar_verbatim_msg), log
-
1
assert diagnostic['stderr'].include?(no_tar_verbatim_msg), diagnostic
-
end
-
-
1
private
-
-
1
def filename_6x9
-
8
starting_files.keys.find { |filename|
-
16
starting_files[filename].include?('6 * 9')
-
}
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def run_cyber_dojo_image_stubbed_with(stub)
-
7
image_stub = "runner_test_stub:#{id}"
-
7
Dir.mktmpdir do |dir|
-
dockerfile = [
-
7
"FROM #{image_name}",
-
'COPY stub /usr/local/bin/red_amber_green.rb'
-
].join("\n")
-
7
IO.write("#{dir}/stub", stub)
-
7
IO.write("#{dir}/Dockerfile", dockerfile)
-
7
shell.assert("docker build --tag #{image_stub} #{dir}")
-
end
-
begin
-
7
run_cyber_dojo_sh({image_name:image_stub})
-
7
image_stub
-
ensure
-
7
shell.assert("docker image rm #{image_stub}")
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_src 'files_delta'
-
-
1
class FilesDeltaTest < TestBase
-
-
1
def self.id58_prefix
-
20
'5C2'
-
end
-
-
1
include FilesDelta
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E76', %w( unchanged content) do
-
1
was_files = { 'wibble.txt' => 'hello' }
-
1
now_files = { 'wibble.txt' => intact('hello') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({}, created)
-
1
assert_equal([], deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E77', %w( changed content ) do
-
1
was_files = { 'wibble.txt' => 'hello' }
-
1
now_files = { 'wibble.txt' => intact('hello, world') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({}, created)
-
1
assert_equal([], deleted)
-
1
assert_equal({'wibble.txt' => intact('hello, world')}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E78', %w( deleted content ) do
-
1
was_files = { 'wibble.txt' => 'hello' }
-
1
now_files = {}
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({}, created)
-
1
assert_equal(['wibble.txt'], deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E79', %w( new content ) do
-
1
was_files = {}
-
1
now_files = { 'wibble.txt' => intact('hello') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({'wibble.txt' => intact('hello')}, created)
-
1
assert_equal([], deleted)
-
1
assert_equal({}, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'E80', %w( new empty content ) do
-
1
was_files = {}
-
1
now_files = { 'empty.file' => intact('') }
-
1
created,deleted,changed = files_delta(was_files, now_files)
-
1
assert_equal({'empty.file' => intact('')}, created)
-
1
assert_equal([], deleted)
-
1
assert_equal({}, changed)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_src 'gnu_zip'
-
1
require_src 'gnu_unzip'
-
-
1
class GnuZipTest < TestBase
-
-
1
def self.id58_prefix
-
4
'CD4'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '4A1', 'simple gzip round-trip' do
-
1
expected = 'sdgfadsfghfghsfhdfghdfghdfgh'
-
1
zipped = Gnu.zip(expected)
-
1
actual = Gnu.unzip(zipped)
-
1
assert_equal expected, actual
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'net/http'
-
-
1
class HttpAdapter
-
-
1
def get(uri)
-
68
KLASS::Get.new(uri)
-
end
-
-
1
def post(uri)
-
KLASS::Post.new(uri)
-
end
-
-
1
def start(hostname, port, req)
-
68
KLASS.start(hostname, port) do |http|
-
68
http.request(req)
-
end
-
end
-
-
1
private
-
-
1
KLASS = Net::HTTP
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class Id58TestTest < TestBase
-
-
1
def self.id58_prefix
-
24
'89c'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'C80',
-
'test-id is available via environment variable' do
-
1
assert_equal '89cC80', ENV['ID58']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '57B',
-
'test-id is also available via a method',
-
'and is the id58_prefix concatenated with the test-id' do
-
1
assert_equal '89c57B', id58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test '18F',
-
'test-name is available via a method' do
-
1
assert_equal 'test-name is available via a method', name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'D30',
-
'test-name can be long',
-
'and split over many',
-
'comma separated lines',
-
'and will automatically be',
-
'joined with spaces' do
-
1
expected = [
-
'test-name can be long',
-
'and split over many',
-
'comma separated lines',
-
'and will automatically be',
-
'joined with spaces'
-
].join(' ')
-
1
assert_equal expected, name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'D31', %w(
-
test-name can be long
-
and split over many lines
-
with %w syntax
-
and will automatically be
-
joined with spaces
-
) do
-
1
expected = [
-
'test-name can be long',
-
'and split over many lines',
-
'with %w syntax',
-
'and will automatically be',
-
'joined with spaces'
-
].join(' ')
-
1
assert_equal expected, name58
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
test 'e3a', %w( hex digits can be UPPERCASE or lowercase ) do
-
1
assert_equal '89ce3a', ENV['ID58']
-
1
assert_equal '89ce3a', id58
-
end
-
-
end
-
1
require_relative 'test_base'
-
1
require 'stringio'
-
-
1
class LogTest < TestBase
-
-
1
def self.id58_prefix
-
4
'CE4'
-
end
-
-
1
def log
-
1
externals.log
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
test '20C',
-
'logging a string message send it directly to stdout' do
-
1
stdout = captured_stdout {
-
1
log << 'Hello'
-
}
-
1
assert_equal 'Hello', stdout
-
end
-
-
# - - - - - - - - - - - - - - - -
-
-
1
def captured_stdout
-
begin
-
1
old_stdout = $stdout
-
1
$stdout = StringIO.new('', 'w')
-
1
yield
-
1
$stdout.string
-
ensure
-
1
$stdout = old_stdout
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require 'json'
-
-
1
class OsTest < TestBase
-
-
1
def self.id58_prefix
-
8
'669'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
multi_os_test '8A2',
-
%w( os-image correspondence ) do
-
2
etc_issue = assert_cyber_dojo_sh('cat /etc/issue')
-
diagnostic = [
-
2
"image_name=:#{image_name}:",
-
"did not find #{os} in etc/issue",
-
etc_issue
-
].join("\n")
-
2
assert etc_issue.include?(os.to_s), diagnostic
-
end
-
-
end
-
1
require_relative 'bash_stub_raiser'
-
1
require_relative 'bash_stub_tar_pipe_out'
-
1
require_relative 'rack_request_stub'
-
1
require_relative 'test_base'
-
1
require_src 'rack_dispatcher'
-
1
require 'json'
-
1
require 'stringio'
-
-
1
class RackDispatcherTest < TestBase
-
-
1
def self.id58_prefix
-
56
'D06'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'BAF',
-
%w( unknown path becomes exception ) do
-
1
expected = 'unknown path'
-
1
assert_rack_call_exception(expected, nil, '{}')
-
1
assert_rack_call_exception(expected, [], '{}')
-
1
assert_rack_call_exception(expected, {}, '{}')
-
1
assert_rack_call_exception(expected, true, '{}')
-
1
assert_rack_call_exception(expected, 42, '{}')
-
1
assert_rack_call_exception(expected, 'unknown', '{}')
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'BB0',
-
%w( malformed json in http payload becomes exception ) do
-
1
expected = 'body is not JSON'
-
1
METHOD_NAMES.each do |method_name|
-
3
assert_rack_call_exception(expected, method_name, 'sdfsdf')
-
3
assert_rack_call_exception(expected, method_name, 'nil')
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'BB1',
-
%w( json not Hash in http payload becomes exception ) do
-
1
expected = 'body is not JSON Hash'
-
1
METHOD_NAMES.each do |method_name|
-
3
assert_rack_call_exception(expected, method_name, 'null')
-
3
assert_rack_call_exception(expected, method_name, '[]')
-
3
assert_rack_call_exception(expected, method_name, 'true')
-
3
assert_rack_call_exception(expected, method_name, '42')
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# missing arguments
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'AA2',
-
%w( missing image_name becomes exception ) do
-
1
assert_rack_call_run_missing(:image_name)
-
end
-
-
1
test 'AA3',
-
%w( missing id becomes exception ) do
-
1
assert_rack_call_run_missing(:id)
-
end
-
-
1
test 'AA4',
-
%w( missing max_seconds becomes exception ) do
-
1
assert_rack_call_run_missing(:max_seconds)
-
end
-
-
1
test 'AA5',
-
%w( missing max_seconds becomes exception ) do
-
1
assert_rack_call_run_missing(:files)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# empty body behave as {}
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '82d', %w(
-
allow '' instead of {} to allow kubernetes
-
liveness/readyness http probes ) do
-
1
rack_call(body:'', path_info:'ready')
-
1
ready = assert_200('ready?')
-
1
assert ready
-
1
assert_nothing_logged
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# sha
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'AB0', 'sha' do
-
1
rack_call({ body:{}.to_json, path_info:'sha' })
-
1
sha = assert_200('sha')
-
1
assert_sha(sha)
-
1
assert_nothing_logged
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# alive?
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '15D', 'its alive' do
-
1
rack_call({ body:{}.to_json, path_info:'alive' })
-
1
alive = assert_200('alive?')
-
1
assert alive
-
1
assert_nothing_logged
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# ready?
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'A9E', 'its ready' do
-
1
rack_call({ body:{}.to_json, path_info:'ready' })
-
1
ready = assert_200('ready?')
-
1
assert ready
-
1
assert_nothing_logged
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# run_cyber_dojo_sh
-
# - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test 'AB5', 'run_cyber_dojo_sh with no logging' do
-
1
args = run_cyber_dojo_sh_args
-
1
rack_call({ path_info:'run_cyber_dojo_sh', body:args.to_json })
-
-
1
assert_200('run_cyber_dojo_sh')
-
1
assert_gcc_starting
-
1
assert_nothing_logged
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
c_assert_test 'AB6', 'run_cyber_dojo_sh with some logging' do
-
1
args = run_cyber_dojo_sh_args
-
1
env = { path_info:'run_cyber_dojo_sh', body:args.to_json }
-
1
stub = BashStubTarPipeOut.new('fail')
-
1
rack_call(env, Externals.new({ 'bash' => stub }))
-
-
1
assert stub.fired_once?
-
1
assert_200('run_cyber_dojo_sh')
-
1
assert_log_contains('command', 'docker exec')
-
1
assert_logged('stdout', 'fail')
-
1
assert_logged('stderr', '')
-
1
assert_logged('status', 1)
-
1
assert_gcc_starting
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test 'AB7', 'server error results in 500 status response' do
-
1
path_info = 'run_cyber_dojo_sh'
-
1
args = run_cyber_dojo_sh_args
-
1
env = { path_info:path_info, body:args.to_json }
-
1
raiser = BashStubRaiser.new('fubar')
-
1
externals = Externals.new({ 'bash' => raiser })
-
1
rack = RackDispatcher.new(externals)
-
1
with_captured_stdout_stderr {
-
1
response = rack.call(env, RackRequestStub)
-
1
assert raiser.fired_once?
-
1
status = response[0]
-
1
assert_equal 500, status
-
}
-
end
-
-
1
private # = = = = = = = = = = = = =
-
-
1
def assert_rack_call_run_missing(name)
-
4
expected = "#{name} is missing"
-
8
args = run_cyber_dojo_sh_args.tap{|hs| hs.delete(name)}.to_json
-
4
assert_rack_call_exception(expected, 'run_cyber_dojo_sh', args)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def assert_rack_call_exception(expected, path_info, body)
-
28
env = { path_info:path_info, body:body }
-
28
rack_call(env)
-
28
assert_400
-
-
28
[@body, @stderr].each do |s|
-
56
refute_nil s
-
56
json = JSON.parse(s)
-
56
ex = json['exception']
-
56
refute_nil ex
-
56
assert_equal 'RunnerService', ex['class']
-
56
assert_equal expected, ex['message']
-
56
assert_equal 'Array', ex['backtrace'].class.name
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def rack_call(env, e = externals)
-
34
rack = RackDispatcher.new(e)
-
34
response = with_captured_stdout_stderr {
-
34
rack.call(env, RackRequestStub)
-
}
-
34
@status = response[0]
-
34
@type = response[1]
-
34
@body = response[2][0]
-
-
34
expected_type = { 'Content-Type' => 'application/json' }
-
34
assert_equal expected_type, @type, response
-
end
-
-
1
def with_captured_stdout_stderr
-
begin
-
35
old_stdout = $stdout
-
35
old_stderr = $stderr
-
35
$stdout = StringIO.new('', 'w')
-
35
$stderr = StringIO.new('', 'w')
-
35
response = yield
-
35
@stderr = $stderr.string
-
35
@stdout = $stdout.string
-
35
response
-
ensure
-
35
$stderr = old_stderr
-
35
$stdout = old_stdout
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def assert_200(name)
-
6
assert_equal 200, @status
-
6
assert_body_contains(name)
-
6
refute_body_contains('exception')
-
6
refute_body_contains('trace')
-
6
JSON.parse(@body)[name]
-
end
-
-
1
def assert_400
-
28
assert_equal 400, @status, "body:#{@body}"
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def assert_body_contains(key)
-
6
refute_nil @body, '@body is nil'
-
6
json = JSON.parse(@body)
-
6
assert json.has_key?(key), "assert json.has_key?(#{key}) keys are #{json.keys}"
-
end
-
-
1
def refute_body_contains(key)
-
12
refute_nil @body, '@body is nil'
-
12
json = JSON.parse(@body)
-
12
refute json.has_key?(key), "refute json.has_key?(#{key}) keys are #{json.keys}"
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def assert_nothing_logged
-
5
assert_equal '', @stdout, 'stdout is not empty'
-
5
assert_equal '', @stderr, 'stderr is not empty'
-
end
-
-
1
def assert_logged(key, value)
-
3
refute_nil @stdout
-
3
json = JSON.parse(@stdout)
-
3
diagnostic = "log does not contain key:#{key}\n#{@stdout}"
-
3
assert json.has_key?(key), diagnostic
-
3
assert_equal value, json[key], @stdout
-
end
-
-
1
def assert_log_contains(key, value)
-
1
refute_nil @stdout
-
1
json = JSON.parse(@stdout)
-
1
diagnostic = "log does not contain key:#{key}\n#{@stdout}"
-
1
assert json.has_key?(key), diagnostic
-
1
assert json[key].include?(value), @stdout
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def assert_gcc_starting
-
2
result = JSON.parse(@body)['run_cyber_dojo_sh']
-
2
stdout = result['stdout']['content']
-
2
diagnostic = 'stdout is not empty!'
-
2
assert_equal '', stdout, diagnostic
-
2
stderr = result['stderr']['content']
-
2
diagnostic = "Expected stderr to start with #{gcc_assert_stderr}"
-
2
assert stderr.start_with?(gcc_assert_stderr), diagnostic
-
2
assert_equal 2, result['status'], :status
-
end
-
-
1
def gcc_assert_stderr
-
# This depends partly on the host-OS. For example, when
-
# the host-OS is CoreLinux (in the boot2docker VM
-
# in DockerToolbox for Mac) then the output ends
-
# ...Aborted (core dumped).
-
# But if the host-OS is Debian/Ubuntu (eg on Travis)
-
# then the output does not say "(core dumped)"
-
# Note that --ulimit core=0 is in place in the runner so
-
# no core file is -actually- dumped.
-
4
"test: hiker.tests.c:6: life_the_universe_and_everything: Assertion `answer() == 42' failed.\n" +
-
"make: *** [makefile:19: test.output] Aborted"
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
def run_cyber_dojo_sh_args
-
{
-
7
image_name:image_name,
-
id:id,
-
files:starting_files,
-
max_seconds:10
-
}
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
METHOD_NAMES = %w(
-
1
sha
-
ready
-
run_cyber_dojo_sh
-
)
-
-
end
-
# frozen_string_literal: true
-
1
require 'ostruct'
-
-
1
class RackRequestStub
-
-
1
def initialize(env)
-
35
@env = env
-
end
-
-
1
def body
-
35
OpenStruct.new(read:@env[:body])
-
end
-
-
1
def path_info
-
35
"/#{@env[:path_info]}"
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
require 'json'
-
1
require 'uri'
-
-
1
module HttpJson
-
-
1
class Requester
-
-
1
def initialize(http, hostname, port)
-
68
@http = http
-
68
@hostname = hostname
-
68
@port = port
-
end
-
-
1
def get(path, args)
-
68
request(path, args) do |uri|
-
68
@http.get(uri)
-
end
-
end
-
-
1
def post(path, args)
-
request(path, args) do |uri|
-
@http.post(uri)
-
end
-
end
-
-
1
private
-
-
1
def request(path, args)
-
68
uri = URI.parse("http://#{@hostname}:#{@port}/#{path}")
-
68
req = yield uri
-
68
req.content_type = 'application/json'
-
68
req.body = JSON.fast_generate(args)
-
68
@http.start(@hostname, @port, req)
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
require 'json'
-
-
1
module HttpJson
-
-
1
class Responder
-
-
1
def initialize(requester, exception_class)
-
68
@requester = requester
-
68
@exception_class = exception_class
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def get(path, args)
-
68
response = @requester.get(path, args)
-
68
unpacked(response.body, path.to_s)
-
rescue => error
-
fail @exception_class, error.message
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def post(path, args)
-
response = @requester.post(path, args)
-
unpacked(response.body, path.to_s)
-
rescue => error
-
fail @exception_class, error.message
-
end
-
-
1
private
-
-
1
def unpacked(body, path)
-
68
json = json_parse(body)
-
68
unless json.is_a?(Hash)
-
fail error_msg(body, 'is not JSON Hash')
-
end
-
68
if json.has_key?('exception')
-
fail JSON.pretty_generate(json['exception'])
-
end
-
68
unless json.has_key?(path)
-
fail error_msg(body, "has no key for '#{path}'")
-
end
-
68
json[path]
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def json_parse(body)
-
68
if body === ''
-
{}
-
else
-
68
JSON.parse!(body)
-
end
-
rescue JSON::ParserError
-
fail error_msg(body, 'is not JSON')
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - -
-
-
1
def error_msg(body, text)
-
"http response.body #{text}:#{body}"
-
end
-
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'requester'
-
1
require_relative 'responder'
-
-
1
module HttpJson
-
-
1
def self.service(http, hostname, port, exception_class)
-
68
requester = Requester.new(http, hostname, port)
-
68
Responder.new(requester, exception_class)
-
end
-
-
end
-
# frozen_string_literal: true
-
-
1
require_relative 'http_json/service'
-
-
1
class LanguagesStartPoints
-
-
1
class Error < RuntimeError
-
1
def initialize(message)
-
super
-
end
-
end
-
-
1
def initialize(http)
-
68
@http = HttpJson::service(http, 'languages-start-points', 4524, Error)
-
end
-
-
1
def names
-
@http.get(__method__, {})
-
end
-
-
1
def manifest(name)
-
68
@http.get(__method__, { name:name })
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_src 'shell_assert_error'
-
-
1
class ShellAssertErrorTest < TestBase
-
-
1
def self.id58_prefix
-
16
'D0F'
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
BAD_UTF8 = "\255"
-
-
1
test '1CA', %w( check illegal/malformed utf8 test data ) do
-
2
error = assert_raises(ArgumentError) { BAD_UTF8.split }
-
1
assert_equal 'invalid byte sequence in UTF-8', error.message
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
test '1CB',
-
%w( bad-utf-8 in command is converted ) do
-
1
ShellAssertError.new(BAD_UTF8,'','',0)
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
test '1CC',
-
%w( bad-utf-8 in stdout is converted ) do
-
1
ShellAssertError.new('',BAD_UTF8,'',0)
-
end
-
-
# - - - - - - - - - - - - - - - - - - -
-
-
1
test '1CD',
-
%w( bad-utf-8 in stderr is converted ) do
-
1
ShellAssertError.new('','',BAD_UTF8,0)
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
-
1
class ShellTest < TestBase
-
-
1
def self.id58_prefix
-
28
'C89'
-
end
-
-
1
def shell
-
7
externals.shell
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# shell.exec(command)
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '243',
-
%w( when exec(command) raises an exception,
-
then the exception is untouched
-
then nothing is logged
-
) do
-
1
log = with_captured_log {
-
2
error = assert_raises(Errno::ENOENT) { shell.exec('xxx Hello') }
-
1
expected = 'No such file or directory - xxx'
-
1
assert_equal expected, error.message, :error_message
-
}
-
1
assert_equal '', log, :log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '244',
-
%w( when exec(command)'s status is zero,
-
it does not raise,
-
it returns [stdout,stderr,status],
-
it logs nothing
-
) do
-
1
log = with_captured_log {
-
1
stdout,stderr,status = shell.exec('printf Hello')
-
1
assert_equal 'Hello', stdout, :stdout
-
1
assert_equal '', stderr, :stderr
-
1
assert_equal 0, status, :status
-
}
-
1
assert_equal '', log, :log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '245',
-
%w( when exec(command) is non-zero,
-
it does not raise,
-
it returns [stdout,stderr,status],
-
it logs [command,stdout,stderr,status] in json format
-
) do
-
1
command = 'printf Bye && false'
-
1
log = with_captured_log {
-
1
stdout,stderr,status = shell.exec(command)
-
1
assert_equal 'Bye', stdout, :stdout
-
1
assert_equal '', stderr, :stderr
-
1
assert_equal 1, status, :status
-
}
-
1
assert_log_contains(log, 'command', command)
-
1
assert_log_contains(log, 'stdout', 'Bye')
-
1
assert_log_contains(log, 'stderr', '')
-
1
assert_log_contains(log, 'status', 1)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# shell.assert(command)
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '247',
-
%w( when assert(command) has status of zero,
-
it returns stdout,
-
it logs nothing
-
) do
-
1
log = with_captured_log {
-
1
stdout = shell.assert('printf Hello')
-
1
assert_equal 'Hello', stdout, :stdout
-
}
-
1
assert_equal '', log, :log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '248',
-
%w( when assert(command) has a status of non-zero,
-
it raises a ShellAssertError holding [command,stdout,stderr,status],
-
it logs [command,stdout,stderr,status]
-
) do
-
1
command = 'printf Hello && false'
-
1
log = with_captured_log {
-
2
error = assert_raises(ShellAssertError) { shell.assert(command) }
-
1
assert_error_contains(error, 'command', command)
-
1
assert_error_contains(error, 'stdout', 'Hello')
-
1
assert_error_contains(error, 'stderr', '')
-
1
assert_error_contains(error, 'status', 1)
-
}
-
1
assert_log_contains(log, 'command', command)
-
1
assert_log_contains(log, 'stdout', 'Hello')
-
1
assert_log_contains(log, 'stderr', '')
-
1
assert_log_contains(log, 'status', 1)
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '249', %w(
-
when assert(command) raises
-
the exception is untouched,
-
it logs nothing
-
) do
-
1
log = with_captured_log {
-
2
error = assert_raises(Errno::ENOENT) { shell.assert('xxx Hello') }
-
1
expected = 'No such file or directory - xxx'
-
1
assert_equal expected, error.message, :error_message
-
}
-
1
assert_equal '', log, :log
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
# special test for silencing known CircleCI error message
-
# - - - - - - - - - - - - - - - - -
-
-
KNOWN_CIRCLE_CI_WARNING =
-
1
"WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. " +
-
"Memory limited without swap."
-
-
1
test '250',
-
%w( known warning message on CircleCI is not logged - helps reveal other warnings ) do
-
bash_stub =
-
1
Class.new do
-
2
def initialize; @fired_count = 0; end
-
2
def fired?(n); @fired_count === n; end
-
1
def run(command)
-
1
@fired_count += 1
-
1
['',KNOWN_CIRCLE_CI_WARNING,0]
-
end
-
end.new
-
log_spy =
-
1
Class.new do
-
2
def initialize; @fired_count = 0; end
-
2
def fired?(n); @fired_count === n; end
-
1
def <<(_s); @fired_count += 1; end
-
end.new
-
1
@externals = Externals.new({ 'bash' => bash_stub, 'log' => log_spy })
-
1
key = 'CIRCLECI'
-
1
on_circle_ci = ENV.include?(key)
-
begin
-
1
ENV[key] = 'true' unless on_circle_ci
-
1
shell.exec('anything')
-
ensure
-
1
ENV.delete(key) unless on_circle_ci
-
end
-
1
assert bash_stub.fired?(1), 'bash_stub.fired?(1) is false'
-
1
assert log_spy.fired?(0), 'log_spy.fired?(0) is false'
-
end
-
-
1
private
-
-
1
def assert_log_contains(log, key, value)
-
8
refute_nil log
-
8
json = JSON.parse(log)
-
8
diagnostic = "log does not contain key:#{key}\n#{log}"
-
8
assert json.has_key?(key), diagnostic
-
8
assert_equal value, json[key], log
-
end
-
-
1
def assert_error_contains(error, key, value)
-
4
refute_nil error
-
4
refute_nil error.message
-
4
json = JSON.parse(error.message)
-
4
diagnostic = "error.message does not contain key:#{key}\n#{error.message}"
-
4
assert json.has_key?(key), diagnostic
-
4
assert_equal value, json[key], error.message
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_src 'tar_reader'
-
1
require_src 'tar_writer'
-
-
1
class TarTest < TestBase
-
-
1
def self.id58_prefix
-
12
'80B'
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '364', 'simple tar round-trip' do
-
1
writer = Tar::Writer.new
-
expected = {
-
1
'hello.txt' => 'greetings earthlings...',
-
'hiker.c' => '#include <stdio.h>'
-
}
-
1
expected.each do |filename, content|
-
2
writer.write(filename, content)
-
end
-
1
reader = Tar::Reader.new(writer.tar_file)
-
1
actual = reader.files
-
1
assert_equal expected, actual
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '365', 'writing content where .size != .bytesize does not throw' do
-
1
utf8 = [226].pack('U*')
-
1
refute_equal utf8.size, utf8.bytesize
-
1
Tar::Writer.new.write('hello.txt', utf8)
-
1
assert doesnt_throw=true
-
end
-
-
# - - - - - - - - - - - - - - - - - -
-
-
1
test '366', 'empty file round-trip' do
-
1
writer = Tar::Writer.new
-
1
filename = 'greeting.txt'
-
1
writer.write(filename, '')
-
1
read = Tar::Reader.new(writer.tar_file).files[filename]
-
1
assert_equal '', read
-
end
-
-
end
-
1
require_relative '../id58_test_base'
-
1
require_relative 'http_adapter'
-
1
require_relative 'services/languages_start_points'
-
1
require_src 'externals'
-
1
require_src 'runner'
-
1
require 'stringio'
-
-
1
class TestBase < Id58TestBase
-
-
1
def initialize(arg)
-
112
super(arg)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def externals
-
121
@externals ||= Externals.new
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def runner
-
80
@runner ||= Runner.new(externals)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def shell
-
16
externals.shell
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def alive?
-
1
runner.alive?['alive?']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def ready?
-
1
runner.ready?['ready?']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def sha
-
1
runner.sha['sha']
-
end
-
-
1
def assert_sha(string)
-
2
assert_equal 40, string.size
-
2
string.each_char do |ch|
-
80
assert '0123456789abcdef'.include?(ch)
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def run_cyber_dojo_sh(named_args = {})
-
77
unchanged_files = starting_files
-
-
77
created_files = defaulted_arg(named_args, :created, {})
-
77
created_files.keys.each do |filename|
-
8
info = "#{filename} is not a created_file (it already exists)"
-
8
refute unchanged_files.keys.include?(filename), info
-
end
-
-
77
changed_files = defaulted_arg(named_args, :changed, {})
-
77
changed_files.keys.each do |filename|
-
61
info = "#{filename} is not a changed_file (it does not already exist)"
-
61
assert unchanged_files.keys.include?(filename), info
-
61
unchanged_files.delete(filename)
-
end
-
-
77
args = []
-
77
args << defaulted_arg(named_args, :image_name, image_name)
-
77
args << id
-
77
args << [ *unchanged_files, *changed_files, *created_files ].to_h
-
77
args << defaulted_arg(named_args, :max_seconds, 10)
-
77
@result = runner.run_cyber_dojo_sh(*args)
-
nil
-
end
-
-
1
attr_reader :result
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def defaulted_arg(named_args, arg_name, arg_default)
-
308
named_args.key?(arg_name) ? named_args[arg_name] : arg_default
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def stdout
-
62
result['run_cyber_dojo_sh'][:stdout]['content']
-
end
-
-
1
def stderr
-
13
result['run_cyber_dojo_sh'][:stderr]['content']
-
end
-
-
1
def timed_out?
-
43
result['run_cyber_dojo_sh'][:timed_out]
-
end
-
-
1
def created
-
68
result['run_cyber_dojo_sh'][:created]
-
end
-
-
1
def deleted
-
34
result['run_cyber_dojo_sh'][:deleted]
-
end
-
-
1
def changed
-
35
result['run_cyber_dojo_sh'][:changed]
-
end
-
-
1
def colour
-
17
result['colour']
-
end
-
-
1
def diagnostic
-
63
result['diagnostic']
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_timed_out
-
2
assert timed_out?, result
-
end
-
-
1
def refute_timed_out
-
38
refute timed_out?, result
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_created(expected)
-
20
assert_hash_equal(expected, created)
-
end
-
-
1
def assert_deleted(expected)
-
22
assert_equal(expected, deleted)
-
end
-
-
1
def assert_changed(expected)
-
22
assert_hash_equal(expected, changed)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_cyber_dojo_sh(script)
-
named_args = {
-
37
:changed => { 'cyber-dojo.sh' => script }
-
}
-
37
run_cyber_dojo_sh(named_args)
-
37
refute_timed_out
-
37
stdout
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def image_name
-
98
manifest['image_name']
-
end
-
-
1
def id
-
103
id58[0..5]
-
end
-
-
1
def uid
-
18
41966
-
end
-
-
1
def group
-
18
'sandbox'
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def starting_files
-
117
manifest['visible_files'].map do |filename,file|
-
367
[ filename, file['content'] ]
-
end.to_h
-
end
-
-
1
def manifest
-
221
@manifest ||= languages_start_points.manifest(display_name)
-
end
-
-
1
def languages_start_points
-
68
LanguagesStartPoints.new(http)
-
end
-
-
1
def http
-
68
HttpAdapter.new
-
end
-
-
1
def self.test(hex_suffix, *lines, &block)
-
61
alpine_test(hex_suffix, *lines, &block)
-
end
-
-
1
def self.alpine_test(hex_suffix, *lines, &block)
-
83
define_test(:Alpine, 'C#, NUnit', hex_suffix, *lines, &block)
-
end
-
-
1
def self.ubuntu_test(hex_suffix, *lines, &block)
-
22
define_test(:Ubuntu, 'VisualBasic, NUnit', hex_suffix, *lines, &block)
-
end
-
-
1
def self.multi_os_test(hex_suffix, *lines, &block)
-
22
alpine_test(hex_suffix+'0', *lines, &block)
-
22
ubuntu_test(hex_suffix+'1', *lines, &block)
-
end
-
-
1
def self.c_assert_test(hex_suffix, *lines, &block)
-
6
define_test(:Debian, 'C (gcc), assert', hex_suffix, *lines, &block)
-
end
-
-
1
def self.clang_assert_test(hex_suffix, *lines, &block)
-
1
define_test(:Ubuntu, 'C (clang), assert', hex_suffix, *lines, &block)
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def with_captured_log
-
begin
-
16
old_stdout = $stdout
-
16
$stdout = StringIO.new('', 'w')
-
16
yield
-
16
$stdout.string
-
ensure
-
16
$stdout = old_stdout
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def intact(content)
-
21
{ 'content' => content, 'truncated' => false }
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def assert_hash_equal(expected, actual)
-
42
assert_equal expected.keys.sort, actual.keys.sort
-
42
expected.keys.each do |key|
-
14
assert_equal expected[key], actual[key], key
-
end
-
end
-
-
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
1
def stat_cmd
-
# Works on Ubuntu and Alpine
-
14
'stat -c "%n %A %u %G %s %y" *'
-
# hiker.h -rw-r--r-- 40045 cyber-dojo 136 2016-06-05 07:03:14.539952547
-
# | | | | | | |
-
# filename permissions uid group size date time
-
# 0 1 2 3 4 5 6
-
-
# Stat
-
# %z == time of last status change
-
# %y == time of last data modification <<=====
-
# %x == time of last access
-
# %w == time of file birth
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require_relative 'test_base'
-
1
require_src 'utf8_clean'
-
-
1
class Utf8CleanTest < TestBase
-
-
1
def self.id58_prefix
-
4
'3D9'
-
end
-
-
# - - - - - - - - - - - - - - - - -
-
-
1
test '7FE', %w( cleans invalid encodings ) do
-
1
bad_str = (100..1000).to_a.pack('c*').force_encoding('utf-8')
-
1
refute bad_str.valid_encoding?
-
1
good_str = Utf8.clean(bad_str)
-
1
assert good_str.valid_encoding?
-
end
-
-
end
-
# frozen_string_literal: true
-
#
-
# ipaddr.rb - A class to manipulate an IP address
-
#
-
# Copyright (c) 2002 Hajimu UMEMOTO <ume@mahoroba.org>.
-
# Copyright (c) 2007, 2009, 2012 Akinori MUSHA <knu@iDaemons.org>.
-
# All rights reserved.
-
#
-
# You can redistribute and/or modify it under the same terms as Ruby.
-
#
-
# $Id: ipaddr.rb 66432 2018-12-18 05:09:08Z knu $
-
#
-
# Contact:
-
# - Akinori MUSHA <knu@iDaemons.org> (current maintainer)
-
#
-
# TODO:
-
# - scope_id support
-
#
-
1
require 'socket'
-
-
# IPAddr provides a set of methods to manipulate an IP address. Both IPv4 and
-
# IPv6 are supported.
-
#
-
# == Example
-
#
-
# require 'ipaddr'
-
#
-
# ipaddr1 = IPAddr.new "3ffe:505:2::1"
-
#
-
# p ipaddr1 #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0001/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff>
-
#
-
# p ipaddr1.to_s #=> "3ffe:505:2::1"
-
#
-
# ipaddr2 = ipaddr1.mask(48) #=> #<IPAddr: IPv6:3ffe:0505:0002:0000:0000:0000:0000:0000/ffff:ffff:ffff:0000:0000:0000:0000:0000>
-
#
-
# p ipaddr2.to_s #=> "3ffe:505:2::"
-
#
-
# ipaddr3 = IPAddr.new "192.168.2.0/24"
-
#
-
# p ipaddr3 #=> #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
-
-
1
class IPAddr
-
-
# 32 bit mask for IPv4
-
1
IN4MASK = 0xffffffff
-
# 128 bit mask for IPv6
-
1
IN6MASK = 0xffffffffffffffffffffffffffffffff
-
# Format string for IPv6
-
1
IN6FORMAT = (["%.4x"] * 8).join(':')
-
-
# Regexp _internally_ used for parsing IPv4 address.
-
1
RE_IPV4ADDRLIKE = %r{
-
\A
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
\z
-
}x
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_FULL = %r{
-
\A
-
(?:
-
(?: [\da-f]{1,4} : ){7} [\da-f]{1,4}
-
|
-
( (?: [\da-f]{1,4} : ){6} )
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
\z
-
}xi
-
-
# Regexp _internally_ used for parsing IPv6 address.
-
1
RE_IPV6ADDRLIKE_COMPRESSED = %r{
-
\A
-
( (?: (?: [\da-f]{1,4} : )* [\da-f]{1,4} )? )
-
::
-
( (?:
-
( (?: [\da-f]{1,4} : )* )
-
(?:
-
[\da-f]{1,4}
-
|
-
(\d+) \. (\d+) \. (\d+) \. (\d+)
-
)
-
)? )
-
\z
-
}xi
-
-
# Generic IPAddr related error. Exceptions raised in this class should
-
# inherit from Error.
-
1
class Error < ArgumentError; end
-
-
# Raised when the provided IP address is an invalid address.
-
1
class InvalidAddressError < Error; end
-
-
# Raised when the address family is invalid such as an address with an
-
# unsupported family, an address with an inconsistent family, or an address
-
# who's family cannot be determined.
-
1
class AddressFamilyError < Error; end
-
-
# Raised when the address is an invalid length.
-
1
class InvalidPrefixError < InvalidAddressError; end
-
-
# Returns the address family of this IP address.
-
1
attr_reader :family
-
-
# Creates a new ipaddr containing the given network byte ordered
-
# string form of an IP address.
-
1
def self.new_ntoh(addr)
-
return new(ntop(addr))
-
end
-
-
# Convert a network byte ordered string form of an IP address into
-
# human readable form.
-
1
def self.ntop(addr)
-
case addr.size
-
when 4
-
s = addr.unpack('C4').join('.')
-
when 16
-
s = IN6FORMAT % addr.unpack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return s
-
end
-
-
# Returns a new ipaddr built by bitwise AND.
-
1
def &(other)
-
return self.clone.set(@addr & coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise OR.
-
1
def |(other)
-
return self.clone.set(@addr | coerce_other(other).to_i)
-
end
-
-
# Returns a new ipaddr built by bitwise right-shift.
-
1
def >>(num)
-
return self.clone.set(@addr >> num)
-
end
-
-
# Returns a new ipaddr built by bitwise left shift.
-
1
def <<(num)
-
return self.clone.set(addr_mask(@addr << num))
-
end
-
-
# Returns a new ipaddr built by bitwise negation.
-
1
def ~
-
return self.clone.set(addr_mask(~@addr))
-
end
-
-
# Returns true if two ipaddrs are equal.
-
1
def ==(other)
-
other = coerce_other(other)
-
rescue
-
false
-
else
-
@family == other.family && @addr == other.to_i
-
end
-
-
# Returns a new ipaddr built by masking IP address with the given
-
# prefixlen/netmask. (e.g. 8, 64, "255.255.255.0", etc.)
-
1
def mask(prefixlen)
-
return self.clone.mask!(prefixlen)
-
end
-
-
# Returns true if the given ipaddr is in the range.
-
#
-
# e.g.:
-
# require 'ipaddr'
-
# net1 = IPAddr.new("192.168.2.0/24")
-
# net2 = IPAddr.new("192.168.2.100")
-
# net3 = IPAddr.new("192.168.3.0")
-
# p net1.include?(net2) #=> true
-
# p net1.include?(net3) #=> false
-
1
def include?(other)
-
other = coerce_other(other)
-
if ipv4_mapped?
-
if (@mask_addr >> 32) != 0xffffffffffffffffffffffff
-
return false
-
end
-
mask_addr = (@mask_addr & IN4MASK)
-
addr = (@addr & IN4MASK)
-
family = Socket::AF_INET
-
else
-
mask_addr = @mask_addr
-
addr = @addr
-
family = @family
-
end
-
if other.ipv4_mapped?
-
other_addr = (other.to_i & IN4MASK)
-
other_family = Socket::AF_INET
-
else
-
other_addr = other.to_i
-
other_family = other.family
-
end
-
-
if family != other_family
-
return false
-
end
-
return ((addr & mask_addr) == (other_addr & mask_addr))
-
end
-
1
alias === include?
-
-
# Returns the integer representation of the ipaddr.
-
1
def to_i
-
return @addr
-
end
-
-
# Returns a string containing the IP address representation.
-
1
def to_s
-
str = to_string
-
return str if ipv4?
-
-
str.gsub!(/\b0{1,3}([\da-f]+)\b/i, '\1')
-
loop do
-
break if str.sub!(/\A0:0:0:0:0:0:0:0\z/, '::')
-
break if str.sub!(/\b0:0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0:0\b/, ':')
-
break if str.sub!(/\b0:0:0\b/, ':')
-
break if str.sub!(/\b0:0\b/, ':')
-
break
-
end
-
str.sub!(/:{3,}/, '::')
-
-
if /\A::(ffff:)?([\da-f]{1,4}):([\da-f]{1,4})\z/i =~ str
-
str = sprintf('::%s%d.%d.%d.%d', $1, $2.hex / 256, $2.hex % 256, $3.hex / 256, $3.hex % 256)
-
end
-
-
str
-
end
-
-
# Returns a string containing the IP address representation in
-
# canonical form.
-
1
def to_string
-
return _to_string(@addr)
-
end
-
-
# Returns a network byte ordered string form of the IP address.
-
1
def hton
-
case @family
-
when Socket::AF_INET
-
return [@addr].pack('N')
-
when Socket::AF_INET6
-
return (0..7).map { |i|
-
(@addr >> (112 - 16 * i)) & 0xffff
-
}.pack('n8')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4 address.
-
1
def ipv4?
-
return @family == Socket::AF_INET
-
end
-
-
# Returns true if the ipaddr is an IPv6 address.
-
1
def ipv6?
-
return @family == Socket::AF_INET6
-
end
-
-
# Returns true if the ipaddr is a loopback address.
-
1
def loopback?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x7f000000
-
when Socket::AF_INET6
-
@addr == 1
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a private address. IPv4 addresses
-
# in 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 as defined in RFC
-
# 1918 and IPv6 Unique Local Addresses in fc00::/7 as defined in RFC
-
# 4193 are considered private.
-
1
def private?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xff000000 == 0x0a000000 || # 10.0.0.0/8
-
@addr & 0xfff00000 == 0xac100000 || # 172.16.0.0/12
-
@addr & 0xffff0000 == 0xc0a80000 # 192.168.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xfe00_0000_0000_0000_0000_0000_0000_0000 == 0xfc00_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is a link-local address. IPv4
-
# addresses in 169.254.0.0/16 reserved by RFC 3927 and Link-Local
-
# IPv6 Unicast Addresses in fe80::/10 reserved by RFC 4291 are
-
# considered link-local.
-
1
def link_local?
-
case @family
-
when Socket::AF_INET
-
@addr & 0xffff0000 == 0xa9fe0000 # 169.254.0.0/16
-
when Socket::AF_INET6
-
@addr & 0xffc0_0000_0000_0000_0000_0000_0000_0000 == 0xfe80_0000_0000_0000_0000_0000_0000_0000
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns true if the ipaddr is an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped?
-
return ipv6? && (@addr >> 32) == 0xffff
-
end
-
-
# Returns true if the ipaddr is an IPv4-compatible IPv6 address.
-
1
def ipv4_compat?
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
_ipv4_compat?
-
end
-
-
1
def _ipv4_compat?
-
if !ipv6? || (@addr >> 32) != 0
-
return false
-
end
-
a = (@addr & IN4MASK)
-
return a != 0 && a != 1
-
end
-
-
1
private :_ipv4_compat?
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-mapped IPv6 address.
-
1
def ipv4_mapped
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr | 0xffff00000000, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the native IPv4 address
-
# into an IPv4-compatible IPv6 address.
-
1
def ipv4_compat
-
warn "IPAddr\##{__callee__} is obsolete", uplevel: 1 if $VERBOSE
-
if !ipv4?
-
raise InvalidAddressError, "not an IPv4 address"
-
end
-
return self.clone.set(@addr, Socket::AF_INET6)
-
end
-
-
# Returns a new ipaddr built by converting the IPv6 address into a
-
# native IPv4 address. If the IP address is not an IPv4-mapped or
-
# IPv4-compatible IPv6 address, returns self.
-
1
def native
-
if !ipv4_mapped? && !_ipv4_compat?
-
return self
-
end
-
return self.clone.set(@addr & IN4MASK, Socket::AF_INET)
-
end
-
-
# Returns a string for DNS reverse lookup. It returns a string in
-
# RFC3172 form for an IPv6 address.
-
1
def reverse
-
case @family
-
when Socket::AF_INET
-
return _reverse + ".in-addr.arpa"
-
when Socket::AF_INET6
-
return ip6_arpa
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC3172.
-
1
def ip6_arpa
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.arpa"
-
end
-
-
# Returns a string for DNS reverse lookup compatible with RFC1886.
-
1
def ip6_int
-
if !ipv6?
-
raise InvalidAddressError, "not an IPv6 address"
-
end
-
return _reverse + ".ip6.int"
-
end
-
-
# Returns the successor to the ipaddr.
-
1
def succ
-
return self.clone.set(@addr + 1, @family)
-
end
-
-
# Compares the ipaddr with another.
-
1
def <=>(other)
-
other = coerce_other(other)
-
rescue
-
nil
-
else
-
@addr <=> other.to_i if other.family == @family
-
end
-
1
include Comparable
-
-
# Checks equality used by Hash.
-
1
def eql?(other)
-
return self.class == other.class && self.hash == other.hash && self == other
-
end
-
-
# Returns a hash value used by Hash, Set, and Array classes
-
1
def hash
-
return ([@addr, @mask_addr].hash << 1) | (ipv4? ? 0 : 1)
-
end
-
-
# Creates a Range object for the network address.
-
1
def to_range
-
begin_addr = (@addr & @mask_addr)
-
-
case @family
-
when Socket::AF_INET
-
end_addr = (@addr | (IN4MASK ^ @mask_addr))
-
when Socket::AF_INET6
-
end_addr = (@addr | (IN6MASK ^ @mask_addr))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
-
return clone.set(begin_addr, @family)..clone.set(end_addr, @family)
-
end
-
-
# Returns the prefix length in bits for the ipaddr.
-
1
def prefix
-
case @family
-
when Socket::AF_INET
-
n = IN4MASK ^ @mask_addr
-
i = 32
-
when Socket::AF_INET6
-
n = IN6MASK ^ @mask_addr
-
i = 128
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
while n.positive?
-
n >>= 1
-
i -= 1
-
end
-
i
-
end
-
-
# Sets the prefix length in bits
-
1
def prefix=(prefix)
-
case prefix
-
when Integer
-
mask!(prefix)
-
else
-
raise InvalidPrefixError, "prefix must be an integer"
-
end
-
end
-
-
# Returns a string containing a human-readable representation of the
-
# ipaddr. ("#<IPAddr: family:address/mask>")
-
1
def inspect
-
case @family
-
when Socket::AF_INET
-
af = "IPv4"
-
when Socket::AF_INET6
-
af = "IPv6"
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
return sprintf("#<%s: %s:%s/%s>", self.class.name,
-
af, _to_string(@addr), _to_string(@mask_addr))
-
end
-
-
1
protected
-
-
# Set +@addr+, the internal stored ip address, to given +addr+. The
-
# parameter +addr+ is validated using the first +family+ member,
-
# which is +Socket::AF_INET+ or +Socket::AF_INET6+.
-
1
def set(addr, *family)
-
case family[0] ? family[0] : @family
-
when Socket::AF_INET
-
if addr < 0 || addr > IN4MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
when Socket::AF_INET6
-
if addr < 0 || addr > IN6MASK
-
raise InvalidAddressError, "invalid address"
-
end
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = addr
-
if family[0]
-
@family = family[0]
-
end
-
return self
-
end
-
-
# Set current netmask to given mask.
-
1
def mask!(mask)
-
case mask
-
when String
-
if mask =~ /\A\d+\z/
-
prefixlen = mask.to_i
-
else
-
m = IPAddr.new(mask)
-
if m.family != @family
-
raise InvalidPrefixError, "address family is not same"
-
end
-
@mask_addr = m.to_i
-
n = @mask_addr ^ m.instance_variable_get(:@mask_addr)
-
unless ((n + 1) & n).zero?
-
raise InvalidPrefixError, "invalid mask #{mask}"
-
end
-
@addr &= @mask_addr
-
return self
-
end
-
else
-
prefixlen = mask
-
end
-
case @family
-
when Socket::AF_INET
-
if prefixlen < 0 || prefixlen > 32
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 32 - prefixlen
-
@mask_addr = ((IN4MASK >> masklen) << masklen)
-
when Socket::AF_INET6
-
if prefixlen < 0 || prefixlen > 128
-
raise InvalidPrefixError, "invalid length"
-
end
-
masklen = 128 - prefixlen
-
@mask_addr = ((IN6MASK >> masklen) << masklen)
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
@addr = ((@addr >> masklen) << masklen)
-
return self
-
end
-
-
1
private
-
-
# Creates a new ipaddr object either from a human readable IP
-
# address representation in string, or from a packed in_addr value
-
# followed by an address family.
-
#
-
# In the former case, the following are the valid formats that will
-
# be recognized: "address", "address/prefixlen" and "address/mask",
-
# where IPv6 address may be enclosed in square brackets (`[' and
-
# `]'). If a prefixlen or a mask is specified, it returns a masked
-
# IP address. Although the address family is determined
-
# automatically from a specified string, you can specify one
-
# explicitly by the optional second argument.
-
#
-
# Otherwise an IP address is generated from a packed in_addr value
-
# and an address family.
-
#
-
# The IPAddr class defines many methods and operators, and some of
-
# those, such as &, |, include? and ==, accept a string, or a packed
-
# in_addr value instead of an IPAddr object.
-
1
def initialize(addr = '::', family = Socket::AF_UNSPEC)
-
if !addr.kind_of?(String)
-
case family
-
when Socket::AF_INET, Socket::AF_INET6
-
set(addr.to_i, family)
-
@mask_addr = (family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
return
-
when Socket::AF_UNSPEC
-
raise AddressFamilyError, "address family must be specified"
-
else
-
raise AddressFamilyError, "unsupported address family: #{family}"
-
end
-
end
-
prefix, prefixlen = addr.split('/')
-
if prefix =~ /\A\[(.*)\]\z/i
-
prefix = $1
-
family = Socket::AF_INET6
-
end
-
# It seems AI_NUMERICHOST doesn't do the job.
-
#Socket.getaddrinfo(left, nil, Socket::AF_INET6, Socket::SOCK_STREAM, nil,
-
# Socket::AI_NUMERICHOST)
-
@addr = @family = nil
-
if family == Socket::AF_UNSPEC || family == Socket::AF_INET
-
@addr = in_addr(prefix)
-
if @addr
-
@family = Socket::AF_INET
-
end
-
end
-
if !@addr && (family == Socket::AF_UNSPEC || family == Socket::AF_INET6)
-
@addr = in6_addr(prefix)
-
@family = Socket::AF_INET6
-
end
-
if family != Socket::AF_UNSPEC && @family != family
-
raise AddressFamilyError, "address family mismatch"
-
end
-
if prefixlen
-
mask!(prefixlen)
-
else
-
@mask_addr = (@family == Socket::AF_INET) ? IN4MASK : IN6MASK
-
end
-
rescue InvalidAddressError => e
-
raise e.class, "#{e.message}: #{addr}"
-
end
-
-
1
def coerce_other(other)
-
case other
-
when IPAddr
-
other
-
when String
-
self.class.new(other)
-
else
-
self.class.new(other, @family)
-
end
-
end
-
-
1
def in_addr(addr)
-
case addr
-
when Array
-
octets = addr
-
else
-
m = RE_IPV4ADDRLIKE.match(addr) or return nil
-
octets = m.captures
-
end
-
octets.inject(0) { |i, s|
-
(n = s.to_i) < 256 or raise InvalidAddressError, "invalid address"
-
s.match(/\A0./) and raise InvalidAddressError, "zero-filled number in IPv4 address is ambiguous"
-
i << 8 | n
-
}
-
end
-
-
1
def in6_addr(left)
-
case left
-
when RE_IPV6ADDRLIKE_FULL
-
if $2
-
addr = in_addr($~[2,4])
-
left = $1 + ':'
-
else
-
addr = 0
-
end
-
right = ''
-
when RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
left.count(':') <= 6 or raise InvalidAddressError, "invalid address"
-
addr = in_addr($~[4,4])
-
left = $1
-
right = $3 + '0:0'
-
else
-
left.count(':') <= ($1.empty? || $2.empty? ? 8 : 7) or
-
raise InvalidAddressError, "invalid address"
-
left = $1
-
right = $2
-
addr = 0
-
end
-
else
-
raise InvalidAddressError, "invalid address"
-
end
-
l = left.split(':')
-
r = right.split(':')
-
rest = 8 - l.size - r.size
-
if rest < 0
-
return nil
-
end
-
(l + Array.new(rest, '0') + r).inject(0) { |i, s|
-
i << 16 | s.hex
-
} | addr
-
end
-
-
1
def addr_mask(addr)
-
case @family
-
when Socket::AF_INET
-
return addr & IN4MASK
-
when Socket::AF_INET6
-
return addr & IN6MASK
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _reverse
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(@addr >> (8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return ("%.32x" % @addr).reverse!.gsub!(/.(?!$)/, '\&.')
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
1
def _to_string(addr)
-
case @family
-
when Socket::AF_INET
-
return (0..3).map { |i|
-
(addr >> (24 - 8 * i)) & 0xff
-
}.join('.')
-
when Socket::AF_INET6
-
return (("%.32x" % addr).gsub!(/.{4}(?!$)/, '\&:'))
-
else
-
raise AddressFamilyError, "unsupported address family"
-
end
-
end
-
-
end
-
-
1
unless Socket.const_defined? :AF_INET6
-
class Socket < BasicSocket
-
# IPv6 protocol family
-
AF_INET6 = Object.new
-
end
-
-
class << IPSocket
-
private
-
-
def valid_v6?(addr)
-
case addr
-
when IPAddr::RE_IPV6ADDRLIKE_FULL
-
if $2
-
$~[2,4].all? {|i| i.to_i < 256 }
-
else
-
true
-
end
-
when IPAddr::RE_IPV6ADDRLIKE_COMPRESSED
-
if $4
-
addr.count(':') <= 6 && $~[4,4].all? {|i| i.to_i < 256}
-
else
-
addr.count(':') <= 7
-
end
-
else
-
false
-
end
-
end
-
-
alias getaddress_orig getaddress
-
-
public
-
-
# Returns a +String+ based representation of a valid DNS hostname,
-
# IPv4 or IPv6 address.
-
#
-
# IPSocket.getaddress 'localhost' #=> "::1"
-
# IPSocket.getaddress 'broadcasthost' #=> "255.255.255.255"
-
# IPSocket.getaddress 'www.ruby-lang.org' #=> "221.186.184.68"
-
# IPSocket.getaddress 'www.ccc.de' #=> "2a00:1328:e102:ccc0::122"
-
def getaddress(s)
-
if valid_v6?(s)
-
s
-
else
-
getaddress_orig(s)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#
-
# = net/http.rb
-
#
-
# Copyright (c) 1999-2007 Yukihiro Matsumoto
-
# Copyright (c) 1999-2007 Minero Aoki
-
# Copyright (c) 2001 GOTOU Yuuzou
-
#
-
# Written and maintained by Minero Aoki <aamine@loveruby.net>.
-
# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
-
#
-
# This file is derived from "http-access.rb".
-
#
-
# Documented by Minero Aoki; converted to RDoc by William Webber.
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms of ruby itself ---
-
# Ruby Distribution License or GNU General Public License.
-
#
-
# See Net::HTTP for an overview and examples.
-
#
-
-
1
require_relative 'protocol'
-
1
require 'uri'
-
1
autoload :OpenSSL, 'openssl'
-
-
1
module Net #:nodoc:
-
-
# :stopdoc:
-
1
class HTTPBadResponse < StandardError; end
-
1
class HTTPHeaderSyntaxError < StandardError; end
-
# :startdoc:
-
-
# == An HTTP client API for Ruby.
-
#
-
# Net::HTTP provides a rich library which can be used to build HTTP
-
# user-agents. For more details about HTTP see
-
# [RFC2616](http://www.ietf.org/rfc/rfc2616.txt).
-
#
-
# Net::HTTP is designed to work closely with URI. URI::HTTP#host,
-
# URI::HTTP#port and URI::HTTP#request_uri are designed to work with
-
# Net::HTTP.
-
#
-
# If you are only performing a few GET requests you should try OpenURI.
-
#
-
# == Simple Examples
-
#
-
# All examples assume you have loaded Net::HTTP with:
-
#
-
# require 'net/http'
-
#
-
# This will also require 'uri' so you don't need to require it separately.
-
#
-
# The Net::HTTP methods in the following section do not persist
-
# connections. They are not recommended if you are performing many HTTP
-
# requests.
-
#
-
# === GET
-
#
-
# Net::HTTP.get('example.com', '/index.html') # => String
-
#
-
# === GET by URI
-
#
-
# uri = URI('http://example.com/index.html?count=10')
-
# Net::HTTP.get(uri) # => String
-
#
-
# === GET with Dynamic Parameters
-
#
-
# uri = URI('http://example.com/index.html')
-
# params = { :limit => 10, :page => 3 }
-
# uri.query = URI.encode_www_form(params)
-
#
-
# res = Net::HTTP.get_response(uri)
-
# puts res.body if res.is_a?(Net::HTTPSuccess)
-
#
-
# === POST
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
-
# puts res.body
-
#
-
# === POST with Multiple Values
-
#
-
# uri = URI('http://www.example.com/search.cgi')
-
# res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50')
-
# puts res.body
-
#
-
# == How to use Net::HTTP
-
#
-
# The following example code can be used as the basis of an HTTP user-agent
-
# which can perform a variety of request types using persistent
-
# connections.
-
#
-
# uri = URI('http://example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Net::HTTP::start immediately creates a connection to an HTTP server which
-
# is kept open for the duration of the block. The connection will remain
-
# open for multiple requests in the block if the server indicates it
-
# supports persistent connections.
-
#
-
# If you wish to re-use a connection across multiple HTTP requests without
-
# automatically closing it you can use ::new and then call #start and
-
# #finish manually.
-
#
-
# The request types Net::HTTP supports are listed below in the section "HTTP
-
# Request Classes".
-
#
-
# For all the Net::HTTP request objects and shortcut request methods you may
-
# supply either a String for the request path or a URI from which Net::HTTP
-
# will extract the request path.
-
#
-
# === Response Data
-
#
-
# uri = URI('http://example.com/index.html')
-
# res = Net::HTTP.get_response(uri)
-
#
-
# # Headers
-
# res['Set-Cookie'] # => String
-
# res.get_fields('set-cookie') # => Array
-
# res.to_hash['set-cookie'] # => Array
-
# puts "Headers: #{res.to_hash.inspect}"
-
#
-
# # Status
-
# puts res.code # => '200'
-
# puts res.message # => 'OK'
-
# puts res.class.name # => 'HTTPOK'
-
#
-
# # Body
-
# puts res.body if res.response_body_permitted?
-
#
-
# === Following Redirection
-
#
-
# Each Net::HTTPResponse object belongs to a class for its response code.
-
#
-
# For example, all 2XX responses are instances of a Net::HTTPSuccess
-
# subclass, a 3XX response is an instance of a Net::HTTPRedirection
-
# subclass and a 200 response is an instance of the Net::HTTPOK class. For
-
# details of response classes, see the section "HTTP Response Classes"
-
# below.
-
#
-
# Using a case statement you can handle various types of responses properly:
-
#
-
# def fetch(uri_str, limit = 10)
-
# # You should choose a better exception.
-
# raise ArgumentError, 'too many HTTP redirects' if limit == 0
-
#
-
# response = Net::HTTP.get_response(URI(uri_str))
-
#
-
# case response
-
# when Net::HTTPSuccess then
-
# response
-
# when Net::HTTPRedirection then
-
# location = response['location']
-
# warn "redirected to #{location}"
-
# fetch(location, limit - 1)
-
# else
-
# response.value
-
# end
-
# end
-
#
-
# print fetch('http://www.ruby-lang.org')
-
#
-
# === POST
-
#
-
# A POST can be made using the Net::HTTP::Post request class. This example
-
# creates a URL encoded POST body:
-
#
-
# uri = URI('http://www.example.com/todo.cgi')
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form_data('from' => '2005-01-01', 'to' => '2005-03-31')
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) do |http|
-
# http.request(req)
-
# end
-
#
-
# case res
-
# when Net::HTTPSuccess, Net::HTTPRedirection
-
# # OK
-
# else
-
# res.value
-
# end
-
#
-
# To send multipart/form-data use Net::HTTPHeader#set_form:
-
#
-
# req = Net::HTTP::Post.new(uri)
-
# req.set_form([['upload', File.open('foo.bar')]], 'multipart/form-data')
-
#
-
# Other requests that can contain a body such as PUT can be created in the
-
# same way using the corresponding request class (Net::HTTP::Put).
-
#
-
# === Setting Headers
-
#
-
# The following example performs a conditional GET using the
-
# If-Modified-Since header. If the files has not been modified since the
-
# time in the header a Not Modified response will be returned. See RFC 2616
-
# section 9.3 for further details.
-
#
-
# uri = URI('http://example.com/cached_response')
-
# file = File.stat 'cached_response'
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req['If-Modified-Since'] = file.mtime.rfc2822
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
#
-
# open 'cached_response', 'w' do |io|
-
# io.write res.body
-
# end if res.is_a?(Net::HTTPSuccess)
-
#
-
# === Basic Authentication
-
#
-
# Basic authentication is performed according to
-
# [RFC2617](http://www.ietf.org/rfc/rfc2617.txt).
-
#
-
# uri = URI('http://example.com/index.html?key=value')
-
#
-
# req = Net::HTTP::Get.new(uri)
-
# req.basic_auth 'user', 'pass'
-
#
-
# res = Net::HTTP.start(uri.hostname, uri.port) {|http|
-
# http.request(req)
-
# }
-
# puts res.body
-
#
-
# === Streaming Response Bodies
-
#
-
# By default Net::HTTP reads an entire response into memory. If you are
-
# handling large files or wish to implement a progress bar you can instead
-
# stream the body directly to an IO.
-
#
-
# uri = URI('http://example.com/large_file')
-
#
-
# Net::HTTP.start(uri.host, uri.port) do |http|
-
# request = Net::HTTP::Get.new uri
-
#
-
# http.request request do |response|
-
# open 'large_file', 'w' do |io|
-
# response.read_body do |chunk|
-
# io.write chunk
-
# end
-
# end
-
# end
-
# end
-
#
-
# === HTTPS
-
#
-
# HTTPS is enabled for an HTTP connection by Net::HTTP#use_ssl=.
-
#
-
# uri = URI('https://secure.example.com/some_path?query=string')
-
#
-
# Net::HTTP.start(uri.host, uri.port, :use_ssl => true) do |http|
-
# request = Net::HTTP::Get.new uri
-
# response = http.request request # Net::HTTPResponse object
-
# end
-
#
-
# Or if you simply want to make a GET request, you may pass in an URI
-
# object that has an HTTPS URL. Net::HTTP automatically turns on TLS
-
# verification if the URI object has a 'https' URI scheme.
-
#
-
# uri = URI('https://example.com/')
-
# Net::HTTP.get(uri) # => String
-
#
-
# In previous versions of Ruby you would need to require 'net/https' to use
-
# HTTPS. This is no longer true.
-
#
-
# === Proxies
-
#
-
# Net::HTTP will automatically create a proxy from the +http_proxy+
-
# environment variable if it is present. To disable use of +http_proxy+,
-
# pass +nil+ for the proxy address.
-
#
-
# You may also create a custom proxy:
-
#
-
# proxy_addr = 'your.proxy.host'
-
# proxy_port = 8080
-
#
-
# Net::HTTP.new('example.com', nil, proxy_addr, proxy_port).start { |http|
-
# # always proxy via your.proxy.addr:8080
-
# }
-
#
-
# See Net::HTTP.new for further details and examples such as proxies that
-
# require a username and password.
-
#
-
# === Compression
-
#
-
# Net::HTTP automatically adds Accept-Encoding for compression of response
-
# bodies and automatically decompresses gzip and deflate responses unless a
-
# Range header was sent.
-
#
-
# Compression can be disabled through the Accept-Encoding: identity header.
-
#
-
# == HTTP Request Classes
-
#
-
# Here is the HTTP request class hierarchy.
-
#
-
# * Net::HTTPRequest
-
# * Net::HTTP::Get
-
# * Net::HTTP::Head
-
# * Net::HTTP::Post
-
# * Net::HTTP::Patch
-
# * Net::HTTP::Put
-
# * Net::HTTP::Proppatch
-
# * Net::HTTP::Lock
-
# * Net::HTTP::Unlock
-
# * Net::HTTP::Options
-
# * Net::HTTP::Propfind
-
# * Net::HTTP::Delete
-
# * Net::HTTP::Move
-
# * Net::HTTP::Copy
-
# * Net::HTTP::Mkcol
-
# * Net::HTTP::Trace
-
#
-
# == HTTP Response Classes
-
#
-
# Here is HTTP response class hierarchy. All classes are defined in Net
-
# module and are subclasses of Net::HTTPResponse.
-
#
-
# HTTPUnknownResponse:: For unhandled HTTP extensions
-
# HTTPInformation:: 1xx
-
# HTTPContinue:: 100
-
# HTTPSwitchProtocol:: 101
-
# HTTPSuccess:: 2xx
-
# HTTPOK:: 200
-
# HTTPCreated:: 201
-
# HTTPAccepted:: 202
-
# HTTPNonAuthoritativeInformation:: 203
-
# HTTPNoContent:: 204
-
# HTTPResetContent:: 205
-
# HTTPPartialContent:: 206
-
# HTTPMultiStatus:: 207
-
# HTTPIMUsed:: 226
-
# HTTPRedirection:: 3xx
-
# HTTPMultipleChoices:: 300
-
# HTTPMovedPermanently:: 301
-
# HTTPFound:: 302
-
# HTTPSeeOther:: 303
-
# HTTPNotModified:: 304
-
# HTTPUseProxy:: 305
-
# HTTPTemporaryRedirect:: 307
-
# HTTPClientError:: 4xx
-
# HTTPBadRequest:: 400
-
# HTTPUnauthorized:: 401
-
# HTTPPaymentRequired:: 402
-
# HTTPForbidden:: 403
-
# HTTPNotFound:: 404
-
# HTTPMethodNotAllowed:: 405
-
# HTTPNotAcceptable:: 406
-
# HTTPProxyAuthenticationRequired:: 407
-
# HTTPRequestTimeOut:: 408
-
# HTTPConflict:: 409
-
# HTTPGone:: 410
-
# HTTPLengthRequired:: 411
-
# HTTPPreconditionFailed:: 412
-
# HTTPRequestEntityTooLarge:: 413
-
# HTTPRequestURITooLong:: 414
-
# HTTPUnsupportedMediaType:: 415
-
# HTTPRequestedRangeNotSatisfiable:: 416
-
# HTTPExpectationFailed:: 417
-
# HTTPUnprocessableEntity:: 422
-
# HTTPLocked:: 423
-
# HTTPFailedDependency:: 424
-
# HTTPUpgradeRequired:: 426
-
# HTTPPreconditionRequired:: 428
-
# HTTPTooManyRequests:: 429
-
# HTTPRequestHeaderFieldsTooLarge:: 431
-
# HTTPUnavailableForLegalReasons:: 451
-
# HTTPServerError:: 5xx
-
# HTTPInternalServerError:: 500
-
# HTTPNotImplemented:: 501
-
# HTTPBadGateway:: 502
-
# HTTPServiceUnavailable:: 503
-
# HTTPGatewayTimeOut:: 504
-
# HTTPVersionNotSupported:: 505
-
# HTTPInsufficientStorage:: 507
-
# HTTPNetworkAuthenticationRequired:: 511
-
#
-
# There is also the Net::HTTPBadResponse exception which is raised when
-
# there is a protocol error.
-
#
-
1
class HTTP < Protocol
-
-
# :stopdoc:
-
1
Revision = %q$Revision: 66401 $.split[1]
-
1
HTTPVersion = '1.1'
-
begin
-
1
require 'zlib'
-
1
require 'stringio' #for our purposes (unpacking gzip) lump these together
-
1
HAVE_ZLIB=true
-
rescue LoadError
-
HAVE_ZLIB=false
-
end
-
# :startdoc:
-
-
# Turns on net/http 1.2 (Ruby 1.8) features.
-
# Defaults to ON in Ruby 1.8 or later.
-
1
def HTTP.version_1_2
-
true
-
end
-
-
# Returns true if net/http is in version 1.2 mode.
-
# Defaults to true.
-
1
def HTTP.version_1_2?
-
true
-
end
-
-
1
def HTTP.version_1_1? #:nodoc:
-
false
-
end
-
-
1
class << HTTP
-
1
alias is_version_1_1? version_1_1? #:nodoc:
-
1
alias is_version_1_2? version_1_2? #:nodoc:
-
end
-
-
#
-
# short cut methods
-
#
-
-
#
-
# Gets the body text from the target and outputs it to $stdout. The
-
# target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# Net::HTTP.get_print URI('http://www.example.com/index.html')
-
#
-
# or:
-
#
-
# Net::HTTP.get_print 'www.example.com', '/index.html'
-
#
-
1
def HTTP.get_print(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port) {|res|
-
res.read_body do |chunk|
-
$stdout.print chunk
-
end
-
}
-
nil
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a string. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# print Net::HTTP.get(URI('http://www.example.com/index.html'))
-
#
-
# or:
-
#
-
# print Net::HTTP.get('www.example.com', '/index.html')
-
#
-
1
def HTTP.get(uri_or_host, path = nil, port = nil)
-
get_response(uri_or_host, path, port).body
-
end
-
-
# Sends a GET request to the target and returns the HTTP response
-
# as a Net::HTTPResponse object. The target can either be specified as
-
# (+uri+), or as (+host+, +path+, +port+ = 80); so:
-
#
-
# res = Net::HTTP.get_response(URI('http://www.example.com/index.html'))
-
# print res.body
-
#
-
# or:
-
#
-
# res = Net::HTTP.get_response('www.example.com', '/index.html')
-
# print res.body
-
#
-
1
def HTTP.get_response(uri_or_host, path = nil, port = nil, &block)
-
if path
-
host = uri_or_host
-
new(host, port || HTTP.default_port).start {|http|
-
return http.request_get(path, &block)
-
}
-
else
-
uri = uri_or_host
-
start(uri.hostname, uri.port,
-
:use_ssl => uri.scheme == 'https') {|http|
-
return http.request_get(uri, &block)
-
}
-
end
-
end
-
-
# Posts data to the specified URI object.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post URI('http://www.example.com/api/search'),
-
# { "q" => "ruby", "max" => "50" }.to_json,
-
# "Content-Type" => "application/json"
-
#
-
1
def HTTP.post(url, data, header = nil)
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.post(url, data, header)
-
}
-
end
-
-
# Posts HTML form data to the specified URI object.
-
# The form data must be provided as a Hash mapping from String to String.
-
# Example:
-
#
-
# { "cmd" => "search", "q" => "ruby", "max" => "50" }
-
#
-
# This method also does Basic Authentication iff +url+.user exists.
-
# But userinfo for authentication is deprecated (RFC3986).
-
# So this feature will be removed.
-
#
-
# Example:
-
#
-
# require 'net/http'
-
# require 'uri'
-
#
-
# Net::HTTP.post_form URI('http://www.example.com/search.cgi'),
-
# { "q" => "ruby", "max" => "50" }
-
#
-
1
def HTTP.post_form(url, params)
-
req = Post.new(url)
-
req.form_data = params
-
req.basic_auth url.user, url.password if url.user
-
start(url.hostname, url.port,
-
:use_ssl => url.scheme == 'https' ) {|http|
-
http.request(req)
-
}
-
end
-
-
#
-
# HTTP session management
-
#
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.default_port
-
http_default_port()
-
end
-
-
# The default port to use for HTTP requests; defaults to 80.
-
1
def HTTP.http_default_port
-
80
-
end
-
-
# The default port to use for HTTPS requests; defaults to 443.
-
1
def HTTP.https_default_port
-
443
-
end
-
-
1
def HTTP.socket_type #:nodoc: obsolete
-
BufferedIO
-
end
-
-
# :call-seq:
-
# HTTP.start(address, port, p_addr, p_port, p_user, p_pass, &block)
-
# HTTP.start(address, port=nil, p_addr=:ENV, p_port=nil, p_user=nil, p_pass=nil, opt, &block)
-
#
-
# Creates a new Net::HTTP object, then additionally opens the TCP
-
# connection and HTTP session.
-
#
-
# Arguments are the following:
-
# _address_ :: hostname or IP address of the server
-
# _port_ :: port of the server
-
# _p_addr_ :: address of proxy
-
# _p_port_ :: port of proxy
-
# _p_user_ :: user of proxy
-
# _p_pass_ :: pass of proxy
-
# _opt_ :: optional hash
-
#
-
# _opt_ sets following values by its accessor.
-
# The keys are ca_file, ca_path, cert, cert_store, ciphers,
-
# close_on_empty_response, key, open_timeout, read_timeout, write_timeout, ssl_timeout,
-
# ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
-
# If you set :use_ssl as true, you can use https and default value of
-
# verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
-
#
-
# If the optional block is given, the newly
-
# created Net::HTTP object is passed to it and closed when the
-
# block finishes. In this case, the return value of this method
-
# is the return value of the block. If no block is given, the
-
# return value of this method is the newly created Net::HTTP object
-
# itself, and the caller is responsible for closing it upon completion
-
# using the finish() method.
-
1
def HTTP.start(address, *arg, &block) # :yield: +http+
-
68
arg.pop if opt = Hash.try_convert(arg[-1])
-
68
port, p_addr, p_port, p_user, p_pass = *arg
-
68
p_addr = :ENV if arg.size < 2
-
68
port = https_default_port if !port && opt && opt[:use_ssl]
-
68
http = new(address, port, p_addr, p_port, p_user, p_pass)
-
-
68
if opt
-
if opt[:use_ssl]
-
opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
-
end
-
http.methods.grep(/\A(\w+)=\z/) do |meth|
-
key = $1.to_sym
-
opt.key?(key) or next
-
http.__send__(meth, opt[key])
-
end
-
end
-
-
68
http.start(&block)
-
end
-
-
1
class << HTTP
-
1
alias newobj new # :nodoc:
-
end
-
-
# Creates a new Net::HTTP object without opening a TCP connection or
-
# HTTP session.
-
#
-
# The +address+ should be a DNS hostname or IP address, the +port+ is the
-
# port the server operates on. If no +port+ is given the default port for
-
# HTTP or HTTPS is used.
-
#
-
# If none of the +p_+ arguments are given, the proxy host and port are
-
# taken from the +http_proxy+ environment variable (or its uppercase
-
# equivalent) if present. If the proxy requires authentication you must
-
# supply it by hand. See URI::Generic#find_proxy for details of proxy
-
# detection from the environment. To disable proxy detection set +p_addr+
-
# to nil.
-
#
-
# If you are connecting to a custom proxy, +p_addr+ specifies the DNS name
-
# or IP address of the proxy host, +p_port+ the port to use to access the
-
# proxy, +p_user+ and +p_pass+ the username and password if authorization
-
# is required to use the proxy, and p_no_proxy hosts which do not
-
# use the proxy.
-
#
-
1
def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil)
-
68
http = super address, port
-
-
68
if proxy_class? then # from Net::HTTP::Proxy()
-
http.proxy_from_env = @proxy_from_env
-
http.proxy_address = @proxy_address
-
http.proxy_port = @proxy_port
-
http.proxy_user = @proxy_user
-
http.proxy_pass = @proxy_pass
-
68
elsif p_addr == :ENV then
-
68
http.proxy_from_env = true
-
else
-
if p_addr && p_no_proxy && !URI::Generic.use_proxy?(p_addr, p_addr, p_port, p_no_proxy)
-
p_addr = nil
-
p_port = nil
-
end
-
http.proxy_address = p_addr
-
http.proxy_port = p_port || default_port
-
http.proxy_user = p_user
-
http.proxy_pass = p_pass
-
end
-
-
68
http
-
end
-
-
# Creates a new Net::HTTP object for the specified server address,
-
# without opening the TCP connection or initializing the HTTP session.
-
# The +address+ should be a DNS hostname or IP address.
-
1
def initialize(address, port = nil)
-
68
@address = address
-
68
@port = (port || HTTP.default_port)
-
68
@local_host = nil
-
68
@local_port = nil
-
68
@curr_http_version = HTTPVersion
-
68
@keep_alive_timeout = 2
-
68
@last_communicated = nil
-
68
@close_on_empty_response = false
-
68
@socket = nil
-
68
@started = false
-
68
@open_timeout = 60
-
68
@read_timeout = 60
-
68
@write_timeout = 60
-
68
@continue_timeout = nil
-
68
@max_retries = 1
-
68
@debug_output = nil
-
-
68
@proxy_from_env = false
-
68
@proxy_uri = nil
-
68
@proxy_address = nil
-
68
@proxy_port = nil
-
68
@proxy_user = nil
-
68
@proxy_pass = nil
-
-
68
@use_ssl = false
-
68
@ssl_context = nil
-
68
@ssl_session = nil
-
68
@sspi_enabled = false
-
68
SSL_IVNAMES.each do |ivname|
-
884
instance_variable_set ivname, nil
-
end
-
end
-
-
1
def inspect
-
"#<#{self.class} #{@address}:#{@port} open=#{started?}>"
-
end
-
-
# *WARNING* This method opens a serious security hole.
-
# Never use this method in production code.
-
#
-
# Sets an output stream for debugging.
-
#
-
# http = Net::HTTP.new(hostname)
-
# http.set_debug_output $stderr
-
# http.start { .... }
-
#
-
1
def set_debug_output(output)
-
warn 'Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
-
@debug_output = output
-
end
-
-
# The DNS host name or IP address to connect to.
-
1
attr_reader :address
-
-
# The port number to connect to.
-
1
attr_reader :port
-
-
# The local host used to establish the connection.
-
1
attr_accessor :local_host
-
-
# The local port used to establish the connection.
-
1
attr_accessor :local_port
-
-
1
attr_writer :proxy_from_env
-
1
attr_writer :proxy_address
-
1
attr_writer :proxy_port
-
1
attr_writer :proxy_user
-
1
attr_writer :proxy_pass
-
-
# Number of seconds to wait for the connection to open. Any number
-
# may be used, including Floats for fractional seconds. If the HTTP
-
# object cannot open a connection in this many seconds, it raises a
-
# Net::OpenTimeout exception. The default value is 60 seconds.
-
1
attr_accessor :open_timeout
-
-
# Number of seconds to wait for one block to be read (via one read(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot read data in this many seconds,
-
# it raises a Net::ReadTimeout exception. The default value is 60 seconds.
-
1
attr_reader :read_timeout
-
-
# Number of seconds to wait for one block to be written (via one write(2)
-
# call). Any number may be used, including Floats for fractional
-
# seconds. If the HTTP object cannot write data in this many seconds,
-
# it raises a Net::WriteTimeout exception. The default value is 60 seconds.
-
# Net::WriteTimeout is not raised on Windows.
-
1
attr_reader :write_timeout
-
-
# Maximum number of times to retry an idempotent request in case of
-
# Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
-
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
-
# Timeout::Error.
-
# Should be a non-negative integer number. Zero means no retries.
-
# The default value is 1.
-
1
def max_retries=(retries)
-
retries = retries.to_int
-
if retries < 0
-
raise ArgumentError, 'max_retries should be non-negative integer number'
-
end
-
@max_retries = retries
-
end
-
-
1
attr_reader :max_retries
-
-
# Setter for the read_timeout attribute.
-
1
def read_timeout=(sec)
-
@socket.read_timeout = sec if @socket
-
@read_timeout = sec
-
end
-
-
# Setter for the write_timeout attribute.
-
1
def write_timeout=(sec)
-
@socket.write_timeout = sec if @socket
-
@write_timeout = sec
-
end
-
-
# Seconds to wait for 100 Continue response. If the HTTP object does not
-
# receive a response in this many seconds it sends the request body. The
-
# default value is +nil+.
-
1
attr_reader :continue_timeout
-
-
# Setter for the continue_timeout attribute.
-
1
def continue_timeout=(sec)
-
@socket.continue_timeout = sec if @socket
-
@continue_timeout = sec
-
end
-
-
# Seconds to reuse the connection of the previous request.
-
# If the idle time is less than this Keep-Alive Timeout,
-
# Net::HTTP reuses the TCP/IP socket used by the previous communication.
-
# The default value is 2 seconds.
-
1
attr_accessor :keep_alive_timeout
-
-
# Returns true if the HTTP session has been started.
-
1
def started?
-
68
@started
-
end
-
-
1
alias active? started? #:nodoc: obsolete
-
-
1
attr_accessor :close_on_empty_response
-
-
# Returns true if SSL/TLS is being used with HTTP.
-
1
def use_ssl?
-
136
@use_ssl
-
end
-
-
# Turn on/off SSL.
-
# This flag must be set before starting session.
-
# If you change use_ssl value after session started,
-
# a Net::HTTP object raises IOError.
-
1
def use_ssl=(flag)
-
flag = flag ? true : false
-
if started? and @use_ssl != flag
-
raise IOError, "use_ssl value changed, but session already started"
-
end
-
@use_ssl = flag
-
end
-
-
1
SSL_IVNAMES = [
-
:@ca_file,
-
:@ca_path,
-
:@cert,
-
:@cert_store,
-
:@ciphers,
-
:@key,
-
:@ssl_timeout,
-
:@ssl_version,
-
:@min_version,
-
:@max_version,
-
:@verify_callback,
-
:@verify_depth,
-
:@verify_mode,
-
]
-
1
SSL_ATTRIBUTES = [
-
:ca_file,
-
:ca_path,
-
:cert,
-
:cert_store,
-
:ciphers,
-
:key,
-
:ssl_timeout,
-
:ssl_version,
-
:min_version,
-
:max_version,
-
:verify_callback,
-
:verify_depth,
-
:verify_mode,
-
]
-
-
# Sets path of a CA certification file in PEM format.
-
#
-
# The file can contain several CA certificates.
-
1
attr_accessor :ca_file
-
-
# Sets path of a CA certification directory containing certifications in
-
# PEM format.
-
1
attr_accessor :ca_path
-
-
# Sets an OpenSSL::X509::Certificate object as client certificate.
-
# (This method is appeared in Michal Rokos's OpenSSL extension).
-
1
attr_accessor :cert
-
-
# Sets the X509::Store to verify peer certificate.
-
1
attr_accessor :cert_store
-
-
# Sets the available ciphers. See OpenSSL::SSL::SSLContext#ciphers=
-
1
attr_accessor :ciphers
-
-
# Sets an OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
-
# (This method is appeared in Michal Rokos's OpenSSL extension.)
-
1
attr_accessor :key
-
-
# Sets the SSL timeout seconds.
-
1
attr_accessor :ssl_timeout
-
-
# Sets the SSL version. See OpenSSL::SSL::SSLContext#ssl_version=
-
1
attr_accessor :ssl_version
-
-
# Sets the minimum SSL version. See OpenSSL::SSL::SSLContext#min_version=
-
1
attr_accessor :min_version
-
-
# Sets the maximum SSL version. See OpenSSL::SSL::SSLContext#max_version=
-
1
attr_accessor :max_version
-
-
# Sets the verify callback for the server certification verification.
-
1
attr_accessor :verify_callback
-
-
# Sets the maximum depth for the certificate chain verification.
-
1
attr_accessor :verify_depth
-
-
# Sets the flags for server the certification verification at beginning of
-
# SSL/TLS session.
-
#
-
# OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
-
1
attr_accessor :verify_mode
-
-
# Returns the X.509 certificates the server presented.
-
1
def peer_cert
-
if not use_ssl? or not @socket
-
return nil
-
end
-
@socket.io.peer_cert
-
end
-
-
# Opens a TCP connection and HTTP session.
-
#
-
# When this method is called with a block, it passes the Net::HTTP
-
# object to the block, and closes the TCP connection and HTTP session
-
# after the block has been executed.
-
#
-
# When called with a block, it returns the return value of the
-
# block; otherwise, it returns self.
-
#
-
1
def start # :yield: http
-
68
raise IOError, 'HTTP session already opened' if @started
-
68
if block_given?
-
begin
-
68
do_start
-
68
return yield(self)
-
ensure
-
68
do_finish
-
end
-
end
-
do_start
-
self
-
end
-
-
1
def do_start
-
68
connect
-
68
@started = true
-
end
-
1
private :do_start
-
-
1
def connect
-
68
if proxy? then
-
conn_address = proxy_address
-
conn_port = proxy_port
-
else
-
68
conn_address = address
-
68
conn_port = port
-
end
-
-
68
D "opening connection to #{conn_address}:#{conn_port}..."
-
68
s = Timeout.timeout(@open_timeout, Net::OpenTimeout) {
-
begin
-
68
TCPSocket.open(conn_address, conn_port, @local_host, @local_port)
-
rescue => e
-
raise e, "Failed to open TCP connection to " +
-
"#{conn_address}:#{conn_port} (#{e.message})"
-
end
-
}
-
68
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
-
68
D "opened"
-
68
if use_ssl?
-
if proxy?
-
plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n"
-
buf << "Host: #{@address}:#{@port}\r\n"
-
if proxy_user
-
credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
-
buf << "Proxy-Authorization: Basic #{credential}\r\n"
-
end
-
buf << "\r\n"
-
plain_sock.write(buf)
-
HTTPResponse.read_new(plain_sock).value
-
# assuming nothing left in buffers after successful CONNECT response
-
end
-
-
ssl_parameters = Hash.new
-
iv_list = instance_variables
-
SSL_IVNAMES.each_with_index do |ivname, i|
-
if iv_list.include?(ivname) and
-
value = instance_variable_get(ivname)
-
ssl_parameters[SSL_ATTRIBUTES[i]] = value if value
-
end
-
end
-
@ssl_context = OpenSSL::SSL::SSLContext.new
-
@ssl_context.set_params(ssl_parameters)
-
@ssl_context.session_cache_mode =
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
-
OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
-
@ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
-
D "starting SSL for #{conn_address}:#{conn_port}..."
-
s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
-
s.sync_close = true
-
# Server Name Indication (SNI) RFC 3546
-
s.hostname = @address if s.respond_to? :hostname=
-
if @ssl_session and
-
Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
-
s.session = @ssl_session
-
end
-
ssl_socket_connect(s, @open_timeout)
-
if @ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
s.post_connection_check(@address)
-
end
-
D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
-
end
-
68
@socket = BufferedIO.new(s, read_timeout: @read_timeout,
-
write_timeout: @write_timeout,
-
continue_timeout: @continue_timeout,
-
debug_output: @debug_output)
-
68
on_connect
-
rescue => exception
-
if s
-
D "Conn close because of connect error #{exception}"
-
s.close
-
end
-
raise
-
end
-
1
private :connect
-
-
1
def on_connect
-
end
-
1
private :on_connect
-
-
# Finishes the HTTP session and closes the TCP connection.
-
# Raises IOError if the session has not been started.
-
1
def finish
-
raise IOError, 'HTTP session not yet started' unless started?
-
do_finish
-
end
-
-
1
def do_finish
-
68
@started = false
-
68
@socket.close if @socket
-
68
@socket = nil
-
end
-
1
private :do_finish
-
-
#
-
# proxy
-
#
-
-
1
public
-
-
# no proxy
-
1
@is_proxy_class = false
-
1
@proxy_from_env = false
-
1
@proxy_addr = nil
-
1
@proxy_port = nil
-
1
@proxy_user = nil
-
1
@proxy_pass = nil
-
-
# Creates an HTTP proxy class which behaves like Net::HTTP, but
-
# performs all access via the specified proxy.
-
#
-
# This class is obsolete. You may pass these same parameters directly to
-
# Net::HTTP.new. See Net::HTTP.new for details of the arguments.
-
1
def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil)
-
return self unless p_addr
-
-
Class.new(self) {
-
@is_proxy_class = true
-
-
if p_addr == :ENV then
-
@proxy_from_env = true
-
@proxy_address = nil
-
@proxy_port = nil
-
else
-
@proxy_from_env = false
-
@proxy_address = p_addr
-
@proxy_port = p_port || default_port
-
end
-
-
@proxy_user = p_user
-
@proxy_pass = p_pass
-
}
-
end
-
-
1
class << HTTP
-
# returns true if self is a class which was created by HTTP::Proxy.
-
1
def proxy_class?
-
68
defined?(@is_proxy_class) ? @is_proxy_class : false
-
end
-
-
# Address of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_address
-
-
# Port number of proxy host. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_port
-
-
# User name for accessing proxy. If Net::HTTP does not use a proxy, nil.
-
1
attr_reader :proxy_user
-
-
# User password for accessing proxy. If Net::HTTP does not use a proxy,
-
# nil.
-
1
attr_reader :proxy_pass
-
end
-
-
# True if requests for this connection will be proxied
-
1
def proxy?
-
136
!!(@proxy_from_env ? proxy_uri : @proxy_address)
-
end
-
-
# True if the proxy for this connection is determined from the environment
-
1
def proxy_from_env?
-
@proxy_from_env
-
end
-
-
# The proxy URI determined from the environment for this connection.
-
1
def proxy_uri # :nodoc:
-
204
return if @proxy_uri == false
-
68
@proxy_uri ||= URI::HTTP.new(
-
"http".freeze, nil, address, port, nil, nil, nil, nil, nil
-
).find_proxy || false
-
68
@proxy_uri || nil
-
end
-
-
# The address of the proxy server, if one is configured.
-
1
def proxy_address
-
if @proxy_from_env then
-
proxy_uri&.hostname
-
else
-
@proxy_address
-
end
-
end
-
-
# The port of the proxy server, if one is configured.
-
1
def proxy_port
-
if @proxy_from_env then
-
proxy_uri&.port
-
else
-
@proxy_port
-
end
-
end
-
-
# [Bug #12921]
-
1
if /linux|freebsd|darwin/ =~ RUBY_PLATFORM
-
1
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = true
-
else
-
ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE = false
-
end
-
-
# The username of the proxy server, if one is configured.
-
1
def proxy_user
-
68
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
68
proxy_uri&.user
-
else
-
@proxy_user
-
end
-
end
-
-
# The password of the proxy server, if one is configured.
-
1
def proxy_pass
-
if ENVIRONMENT_VARIABLE_IS_MULTIUSER_SAFE && @proxy_from_env
-
proxy_uri&.password
-
else
-
@proxy_pass
-
end
-
end
-
-
1
alias proxyaddr proxy_address #:nodoc: obsolete
-
1
alias proxyport proxy_port #:nodoc: obsolete
-
-
1
private
-
-
# without proxy, obsolete
-
-
1
def conn_address # :nodoc:
-
address()
-
end
-
-
1
def conn_port # :nodoc:
-
port()
-
end
-
-
1
def edit_path(path)
-
68
if proxy?
-
if path.start_with?("ftp://") || use_ssl?
-
path
-
else
-
"http://#{addr_port}#{path}"
-
end
-
else
-
68
path
-
end
-
end
-
-
#
-
# HTTP operations
-
#
-
-
1
public
-
-
# Retrieves data from +path+ on the connected-to host which may be an
-
# absolute path String or a URI to extract the path from.
-
#
-
# +initheader+ must be a Hash like { 'Accept' => '*/*', ... },
-
# and it defaults to an empty hash.
-
# If +initheader+ doesn't have the key 'accept-encoding', then
-
# a value of "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" is used,
-
# so that gzip compression is used in preference to deflate
-
# compression, which is used in preference to no compression.
-
# Ruby doesn't have libraries to support the compress (Lempel-Ziv)
-
# compression, so that is not supported. The intent of this is
-
# to reduce bandwidth by default. If this routine sets up
-
# compression, then it does the decompression also, removing
-
# the header as well to prevent confusion. Otherwise
-
# it leaves the body as it found it.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises an exception.
-
#
-
# response = http.get('/index.html')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.get('/~foo/') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
1
def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
res = nil
-
request(Get.new(path, initheader)) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
# Gets only the header from +path+ on the connected-to host.
-
# +header+ is a Hash like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# This method never raises an exception.
-
#
-
# response = nil
-
# Net::HTTP.start('some.www.server', 80) {|http|
-
# response = http.head('/index.html')
-
# }
-
# p response['content-type']
-
#
-
1
def head(path, initheader = nil)
-
request(Head.new(path, initheader))
-
end
-
-
# Posts +data+ (must be a String) to +path+. +header+ must be a Hash
-
# like { 'Accept' => '*/*', ... }.
-
#
-
# This method returns a Net::HTTPResponse object.
-
#
-
# If called with a block, yields each fragment of the
-
# entity body in turn as a string as it is read from
-
# the socket. Note that in this case, the returned response
-
# object will *not* contain a (meaningful) body.
-
#
-
# +dest+ argument is obsolete.
-
# It still works but you must not use it.
-
#
-
# This method never raises exception.
-
#
-
# response = http.post('/cgi-bin/search.rb', 'query=foo')
-
#
-
# # using block
-
# File.open('result.txt', 'w') {|f|
-
# http.post('/cgi-bin/search.rb', 'query=foo') do |str|
-
# f.write str
-
# end
-
# }
-
#
-
# You should set Content-Type: header field for POST.
-
# If no Content-Type: field given, this method uses
-
# "application/x-www-form-urlencoded" by default.
-
#
-
1
def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Post, &block)
-
end
-
-
# Sends a PATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
-
send_entity(path, data, initheader, dest, Patch, &block)
-
end
-
-
1
def put(path, data, initheader = nil) #:nodoc:
-
request(Put.new(path, initheader), data)
-
end
-
-
# Sends a PROPPATCH request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def proppatch(path, body, initheader = nil)
-
request(Proppatch.new(path, initheader), body)
-
end
-
-
# Sends a LOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def lock(path, body, initheader = nil)
-
request(Lock.new(path, initheader), body)
-
end
-
-
# Sends a UNLOCK request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def unlock(path, body, initheader = nil)
-
request(Unlock.new(path, initheader), body)
-
end
-
-
# Sends a OPTIONS request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def options(path, initheader = nil)
-
request(Options.new(path, initheader))
-
end
-
-
# Sends a PROPFIND request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def propfind(path, body = nil, initheader = {'Depth' => '0'})
-
request(Propfind.new(path, initheader), body)
-
end
-
-
# Sends a DELETE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def delete(path, initheader = {'Depth' => 'Infinity'})
-
request(Delete.new(path, initheader))
-
end
-
-
# Sends a MOVE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def move(path, initheader = nil)
-
request(Move.new(path, initheader))
-
end
-
-
# Sends a COPY request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def copy(path, initheader = nil)
-
request(Copy.new(path, initheader))
-
end
-
-
# Sends a MKCOL request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def mkcol(path, body = nil, initheader = nil)
-
request(Mkcol.new(path, initheader), body)
-
end
-
-
# Sends a TRACE request to the +path+ and gets a response,
-
# as an HTTPResponse object.
-
1
def trace(path, initheader = nil)
-
request(Trace.new(path, initheader))
-
end
-
-
# Sends a GET request to the +path+.
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_get('/index.html')
-
# # The entity body is already read in this case.
-
# p response['content-type']
-
# puts response.body
-
#
-
# # Using a block
-
# http.request_get('/index.html') {|response|
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_get(path, initheader = nil, &block) # :yield: +response+
-
request(Get.new(path, initheader), &block)
-
end
-
-
# Sends a HEAD request to the +path+ and returns the response
-
# as a Net::HTTPResponse object.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.request_head('/index.html')
-
# p response['content-type']
-
#
-
1
def request_head(path, initheader = nil, &block)
-
request(Head.new(path, initheader), &block)
-
end
-
-
# Sends a POST request to the +path+.
-
#
-
# Returns the response as a Net::HTTPResponse object.
-
#
-
# When called with a block, the block is passed an HTTPResponse
-
# object. The body of that response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body, if desired.
-
#
-
# Returns the response.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# # example
-
# response = http.request_post('/cgi-bin/nice.rb', 'datadatadata...')
-
# p response.status
-
# puts response.body # body is already read in this case
-
#
-
# # using block
-
# http.request_post('/cgi-bin/nice.rb', 'datadatadata...') {|response|
-
# p response.status
-
# p response['content-type']
-
# response.read_body do |str| # read body now
-
# print str
-
# end
-
# }
-
#
-
1
def request_post(path, data, initheader = nil, &block) # :yield: +response+
-
request Post.new(path, initheader), data, &block
-
end
-
-
1
def request_put(path, data, initheader = nil, &block) #:nodoc:
-
request Put.new(path, initheader), data, &block
-
end
-
-
1
alias get2 request_get #:nodoc: obsolete
-
1
alias head2 request_head #:nodoc: obsolete
-
1
alias post2 request_post #:nodoc: obsolete
-
1
alias put2 request_put #:nodoc: obsolete
-
-
-
# Sends an HTTP request to the HTTP server.
-
# Also sends a DATA string if +data+ is given.
-
#
-
# Returns a Net::HTTPResponse object.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
# response = http.send_request('GET', '/index.html')
-
# puts response.body
-
#
-
1
def send_request(name, path, data = nil, header = nil)
-
has_response_body = name != 'HEAD'
-
r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
-
request r, data
-
end
-
-
# Sends an HTTPRequest object +req+ to the HTTP server.
-
#
-
# If +req+ is a Net::HTTP::Post or Net::HTTP::Put request containing
-
# data, the data is also sent. Providing data for a Net::HTTP::Head or
-
# Net::HTTP::Get request results in an ArgumentError.
-
#
-
# Returns an HTTPResponse object.
-
#
-
# When called with a block, passes an HTTPResponse object to the block.
-
# The body of the response will not have been read yet;
-
# the block can process it using HTTPResponse#read_body,
-
# if desired.
-
#
-
# This method never raises Net::* exceptions.
-
#
-
1
def request(req, body = nil, &block) # :yield: +response+
-
68
unless started?
-
start {
-
req['connection'] ||= 'close'
-
return request(req, body, &block)
-
}
-
end
-
68
if proxy_user()
-
req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
-
end
-
68
req.set_body_internal body
-
68
res = transport_request(req, &block)
-
68
if sspi_auth?(res)
-
sspi_auth(req)
-
res = transport_request(req, &block)
-
end
-
68
res
-
end
-
-
1
private
-
-
# Executes a request which uses a representation
-
# and returns its body.
-
1
def send_entity(path, data, initheader, dest, type, &block)
-
res = nil
-
request(type.new(path, initheader), data) {|r|
-
r.read_body dest, &block
-
res = r
-
}
-
res
-
end
-
-
1
IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :nodoc:
-
-
1
def transport_request(req)
-
68
count = 0
-
begin
-
68
begin_transport req
-
68
res = catch(:response) {
-
68
req.exec @socket, @curr_http_version, edit_path(req.path)
-
68
begin
-
68
res = HTTPResponse.read_new(@socket)
-
68
res.decode_content = req.decode_content
-
end while res.kind_of?(HTTPInformation)
-
-
68
res.uri = req.uri
-
-
68
res
-
}
-
68
res.reading_body(@socket, req.response_body_permitted?) {
-
68
yield res if block_given?
-
}
-
rescue Net::OpenTimeout
-
raise
-
rescue Net::ReadTimeout, IOError, EOFError,
-
Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE,
-
# avoid a dependency on OpenSSL
-
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
-
Timeout::Error => exception
-
if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
-
count += 1
-
@socket.close if @socket
-
D "Conn close because of error #{exception}, and retry"
-
retry
-
end
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise
-
end
-
-
68
end_transport req, res
-
68
res
-
rescue => exception
-
D "Conn close because of error #{exception}"
-
@socket.close if @socket
-
raise exception
-
end
-
-
1
def begin_transport(req)
-
68
if @socket.closed?
-
connect
-
68
elsif @last_communicated
-
if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
D 'Conn close because of keep_alive_timeout'
-
@socket.close
-
connect
-
elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
-
D "Conn close because of EOF"
-
@socket.close
-
connect
-
end
-
end
-
-
68
if not req.response_body_permitted? and @close_on_empty_response
-
req['connection'] ||= 'close'
-
end
-
-
68
req.update_uri address, port, use_ssl?
-
68
req['host'] ||= addr_port()
-
end
-
-
1
def end_transport(req, res)
-
68
@curr_http_version = res.http_version
-
68
@last_communicated = nil
-
68
if @socket.closed?
-
D 'Conn socket closed'
-
68
elsif not res.body and @close_on_empty_response
-
D 'Conn close'
-
@socket.close
-
68
elsif keep_alive?(req, res)
-
D 'Conn keep-alive'
-
@last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
-
else
-
68
D 'Conn close'
-
68
@socket.close
-
end
-
end
-
-
1
def keep_alive?(req, res)
-
68
return false if req.connection_close?
-
68
if @curr_http_version <= '1.0'
-
res.connection_keep_alive?
-
else # HTTP/1.1 or later
-
68
not res.connection_close?
-
end
-
end
-
-
1
def sspi_auth?(res)
-
68
return false unless @sspi_enabled
-
if res.kind_of?(HTTPProxyAuthenticationRequired) and
-
proxy? and res["Proxy-Authenticate"].include?("Negotiate")
-
begin
-
require 'win32/sspi'
-
true
-
rescue LoadError
-
false
-
end
-
else
-
false
-
end
-
end
-
-
1
def sspi_auth(req)
-
n = Win32::SSPI::NegotiateAuth.new
-
req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
-
# Some versions of ISA will close the connection if this isn't present.
-
req["Connection"] = "Keep-Alive"
-
req["Proxy-Connection"] = "Keep-Alive"
-
res = transport_request(req)
-
authphrase = res["Proxy-Authenticate"] or return res
-
req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}"
-
rescue => err
-
raise HTTPAuthenticationError.new('HTTP authentication failed', err)
-
end
-
-
#
-
# utils
-
#
-
-
1
private
-
-
1
def addr_port
-
addr = address
-
addr = "[#{addr}]" if addr.include?(":")
-
default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
-
default_port == port ? addr : "#{addr}:#{port}"
-
end
-
-
1
def D(msg)
-
204
return unless @debug_output
-
@debug_output << msg
-
@debug_output << "\n"
-
end
-
end
-
-
end
-
-
1
require_relative 'http/exceptions'
-
-
1
require_relative 'http/header'
-
-
1
require_relative 'http/generic_request'
-
1
require_relative 'http/request'
-
1
require_relative 'http/requests'
-
-
1
require_relative 'http/response'
-
1
require_relative 'http/responses'
-
-
1
require_relative 'http/proxy_delta'
-
-
1
require_relative 'http/backward'
-
# frozen_string_literal: false
-
# for backward compatibility
-
-
# :enddoc:
-
-
1
class Net::HTTP
-
1
ProxyMod = ProxyDelta
-
end
-
-
1
module Net
-
1
HTTPSession = Net::HTTP
-
end
-
-
1
module Net::NetPrivate
-
1
HTTPRequest = ::Net::HTTPRequest
-
end
-
-
1
Net::HTTPInformationCode = Net::HTTPInformation
-
1
Net::HTTPSuccessCode = Net::HTTPSuccess
-
1
Net::HTTPRedirectionCode = Net::HTTPRedirection
-
1
Net::HTTPRetriableCode = Net::HTTPRedirection
-
1
Net::HTTPClientErrorCode = Net::HTTPClientError
-
1
Net::HTTPFatalErrorCode = Net::HTTPClientError
-
1
Net::HTTPServerErrorCode = Net::HTTPServerError
-
1
Net::HTTPResponceReceiver = Net::HTTPResponse
-
-
# frozen_string_literal: false
-
# Net::HTTP exception class.
-
# You cannot use Net::HTTPExceptions directly; instead, you must use
-
# its subclasses.
-
1
module Net::HTTPExceptions
-
1
def initialize(msg, res) #:nodoc:
-
super msg
-
@response = res
-
end
-
1
attr_reader :response
-
1
alias data response #:nodoc: obsolete
-
end
-
1
class Net::HTTPError < Net::ProtocolError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPRetriableError < Net::ProtoRetriableError
-
1
include Net::HTTPExceptions
-
end
-
1
class Net::HTTPServerException < Net::ProtoServerError
-
# We cannot use the name "HTTPServerError", it is the name of the response.
-
1
include Net::HTTPExceptions
-
end
-
-
# for compatibility
-
1
Net::HTTPClientException = Net::HTTPServerException
-
-
1
class Net::HTTPFatalError < Net::ProtoFatalError
-
1
include Net::HTTPExceptions
-
end
-
-
1
module Net
-
1
deprecate_constant(:HTTPServerException)
-
end
-
# frozen_string_literal: false
-
# HTTPGenericRequest is the parent of the HTTPRequest class.
-
# Do not use this directly; use a subclass of HTTPRequest.
-
#
-
# Mixes in the HTTPHeader module to provide easier access to HTTP headers.
-
#
-
1
class Net::HTTPGenericRequest
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
-
68
@method = m
-
68
@request_has_body = reqbody
-
68
@response_has_body = resbody
-
-
68
if URI === uri_or_path then
-
68
raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path
-
68
raise ArgumentError, "no host component for URI" unless uri_or_path.hostname
-
68
@uri = uri_or_path.dup
-
68
host = @uri.hostname.dup
-
68
host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port
-
68
@path = uri_or_path.request_uri
-
68
raise ArgumentError, "no HTTP request path given" unless @path
-
else
-
@uri = nil
-
host = nil
-
raise ArgumentError, "no HTTP request path given" unless uri_or_path
-
raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
-
@path = uri_or_path.dup
-
end
-
-
68
@decode_content = false
-
-
68
if @response_has_body and Net::HTTP::HAVE_ZLIB then
-
68
if !initheader ||
-
!initheader.keys.any? { |k|
-
%w[accept-encoding range].include? k.downcase
-
} then
-
68
@decode_content = true
-
68
initheader = initheader ? initheader.dup : {}
-
68
initheader["accept-encoding"] =
-
"gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
-
end
-
end
-
-
68
initialize_http_header initheader
-
68
self['Accept'] ||= '*/*'
-
68
self['User-Agent'] ||= 'Ruby'
-
68
self['Host'] ||= host if host
-
68
@body = nil
-
68
@body_stream = nil
-
68
@body_data = nil
-
end
-
-
1
attr_reader :method
-
1
attr_reader :path
-
1
attr_reader :uri
-
-
# Automatically set to false if the user sets the Accept-Encoding header.
-
# This indicates they wish to handle Content-encoding in responses
-
# themselves.
-
1
attr_reader :decode_content
-
-
1
def inspect
-
"\#<#{self.class} #{@method}>"
-
end
-
-
##
-
# Don't automatically decode response content-encoding if the user indicates
-
# they want to handle it.
-
-
1
def []=(key, val) # :nodoc:
-
204
@decode_content = false if key.downcase == 'accept-encoding'
-
-
204
super key, val
-
end
-
-
1
def request_body_permitted?
-
@request_has_body
-
end
-
-
1
def response_body_permitted?
-
136
@response_has_body
-
end
-
-
1
def body_exist?
-
warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
-
response_body_permitted?
-
end
-
-
1
attr_reader :body
-
-
1
def body=(str)
-
68
@body = str
-
68
@body_stream = nil
-
68
@body_data = nil
-
68
str
-
end
-
-
1
attr_reader :body_stream
-
-
1
def body_stream=(input)
-
@body = nil
-
@body_stream = input
-
@body_data = nil
-
input
-
end
-
-
1
def set_body_internal(str) #:nodoc: internal use only
-
68
raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
-
68
self.body = str if str
-
68
if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
-
self.body = ''
-
end
-
end
-
-
#
-
# write
-
#
-
-
1
def exec(sock, ver, path) #:nodoc: internal use only
-
68
if @body
-
68
send_request_with_body sock, ver, path, @body
-
elsif @body_stream
-
send_request_with_body_stream sock, ver, path, @body_stream
-
elsif @body_data
-
send_request_with_body_data sock, ver, path, @body_data
-
else
-
write_header sock, ver, path
-
end
-
end
-
-
1
def update_uri(addr, port, ssl) # :nodoc: internal use only
-
# reflect the connection and @path to @uri
-
68
return unless @uri
-
-
68
if ssl
-
scheme = 'https'.freeze
-
klass = URI::HTTPS
-
else
-
68
scheme = 'http'.freeze
-
68
klass = URI::HTTP
-
end
-
-
68
if host = self['host']
-
68
host.sub!(/:.*/s, ''.freeze)
-
elsif host = @uri.host
-
else
-
host = addr
-
end
-
# convert the class of the URI
-
68
if @uri.is_a?(klass)
-
68
@uri.host = host
-
68
@uri.port = port
-
else
-
@uri = klass.new(
-
scheme, @uri.userinfo,
-
host, port, nil,
-
@uri.path, nil, @uri.query, nil)
-
end
-
end
-
-
1
private
-
-
1
class Chunker #:nodoc:
-
1
def initialize(sock)
-
@sock = sock
-
@prev = nil
-
end
-
-
1
def write(buf)
-
# avoid memcpy() of buf, buf can huge and eat memory bandwidth
-
rv = buf.bytesize
-
@sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
-
rv
-
end
-
-
1
def finish
-
@sock.write("0\r\n\r\n")
-
end
-
end
-
-
1
def send_request_with_body(sock, ver, path, body)
-
68
self.content_length = body.bytesize
-
68
delete 'Transfer-Encoding'
-
68
supply_default_content_type
-
68
write_header sock, ver, path
-
68
wait_for_continue sock, ver if sock.continue_timeout
-
68
sock.write body
-
end
-
-
1
def send_request_with_body_stream(sock, ver, path, f)
-
unless content_length() or chunked?
-
raise ArgumentError,
-
"Content-Length not given and Transfer-Encoding is not `chunked'"
-
end
-
supply_default_content_type
-
write_header sock, ver, path
-
wait_for_continue sock, ver if sock.continue_timeout
-
if chunked?
-
chunker = Chunker.new(sock)
-
IO.copy_stream(f, chunker)
-
chunker.finish
-
else
-
# copy_stream can sendfile() to sock.io unless we use SSL.
-
# If sock.io is an SSLSocket, copy_stream will hit SSL_write()
-
IO.copy_stream(f, sock.io)
-
end
-
end
-
-
1
def send_request_with_body_data(sock, ver, path, params)
-
if /\Amultipart\/form-data\z/i !~ self.content_type
-
self.content_type = 'application/x-www-form-urlencoded'
-
return send_request_with_body(sock, ver, path, URI.encode_www_form(params))
-
end
-
-
opt = @form_option.dup
-
require 'securerandom' unless defined?(SecureRandom)
-
opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
-
self.set_content_type(self.content_type, boundary: opt[:boundary])
-
if chunked?
-
write_header sock, ver, path
-
encode_multipart_form_data(sock, params, opt)
-
else
-
require 'tempfile'
-
file = Tempfile.new('multipart')
-
file.binmode
-
encode_multipart_form_data(file, params, opt)
-
file.rewind
-
self.content_length = file.size
-
write_header sock, ver, path
-
IO.copy_stream(file, sock)
-
file.close(true)
-
end
-
end
-
-
1
def encode_multipart_form_data(out, params, opt)
-
charset = opt[:charset]
-
boundary = opt[:boundary]
-
require 'securerandom' unless defined?(SecureRandom)
-
boundary ||= SecureRandom.urlsafe_base64(40)
-
chunked_p = chunked?
-
-
buf = ''
-
params.each do |key, value, h={}|
-
key = quote_string(key, charset)
-
filename =
-
h.key?(:filename) ? h[:filename] :
-
value.respond_to?(:to_path) ? File.basename(value.to_path) :
-
nil
-
-
buf << "--#{boundary}\r\n"
-
if filename
-
filename = quote_string(filename, charset)
-
type = h[:content_type] || 'application/octet-stream'
-
buf << "Content-Disposition: form-data; " \
-
"name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
-
"Content-Type: #{type}\r\n\r\n"
-
if !out.respond_to?(:write) || !value.respond_to?(:read)
-
# if +out+ is not an IO or +value+ is not an IO
-
buf << (value.respond_to?(:read) ? value.read : value)
-
elsif value.respond_to?(:size) && chunked_p
-
# if +out+ is an IO and +value+ is a File, use IO.copy_stream
-
flush_buffer(out, buf, chunked_p)
-
out << "%x\r\n" % value.size if chunked_p
-
IO.copy_stream(value, out)
-
out << "\r\n" if chunked_p
-
else
-
# +out+ is an IO, and +value+ is not a File but an IO
-
flush_buffer(out, buf, chunked_p)
-
1 while flush_buffer(out, value.read(4096), chunked_p)
-
end
-
else
-
# non-file field:
-
# HTML5 says, "The parts of the generated multipart/form-data
-
# resource that correspond to non-file fields must not have a
-
# Content-Type header specified."
-
buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
-
buf << (value.respond_to?(:read) ? value.read : value)
-
end
-
buf << "\r\n"
-
end
-
buf << "--#{boundary}--\r\n"
-
flush_buffer(out, buf, chunked_p)
-
out << "0\r\n\r\n" if chunked_p
-
end
-
-
1
def quote_string(str, charset)
-
str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
-
str.gsub(/[\\"]/, '\\\\\&')
-
end
-
-
1
def flush_buffer(out, buf, chunked_p)
-
return unless buf
-
out << "%x\r\n"%buf.bytesize if chunked_p
-
out << buf
-
out << "\r\n" if chunked_p
-
buf.clear
-
end
-
-
1
def supply_default_content_type
-
68
return if content_type()
-
warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE
-
set_content_type 'application/x-www-form-urlencoded'
-
end
-
-
##
-
# Waits up to the continue timeout for a response from the server provided
-
# we're speaking HTTP 1.1 and are expecting a 100-continue response.
-
-
1
def wait_for_continue(sock, ver)
-
if ver >= '1.1' and @header['expect'] and
-
@header['expect'].include?('100-continue')
-
if sock.io.to_io.wait_readable(sock.continue_timeout)
-
res = Net::HTTPResponse.read_new(sock)
-
unless res.kind_of?(Net::HTTPContinue)
-
res.decode_content = @decode_content
-
throw :response, res
-
end
-
end
-
end
-
end
-
-
1
def write_header(sock, ver, path)
-
68
reqline = "#{@method} #{path} HTTP/#{ver}"
-
68
if /[\r\n]/ =~ reqline
-
raise ArgumentError, "A Request-Line must not contain CR or LF"
-
end
-
68
buf = ""
-
68
buf << reqline << "\r\n"
-
68
each_capitalized do |k,v|
-
408
buf << "#{k}: #{v}\r\n"
-
end
-
68
buf << "\r\n"
-
68
sock.write buf
-
end
-
-
end
-
-
# frozen_string_literal: false
-
# The HTTPHeader module defines methods for reading and writing
-
# HTTP headers.
-
#
-
# It is used as a mixin by other classes, to provide hash-like
-
# access to HTTP header values. Unlike raw hash access, HTTPHeader
-
# provides access via case-insensitive keys. It also provides
-
# methods for accessing commonly-used HTTP header values in more
-
# convenient formats.
-
#
-
1
module Net::HTTPHeader
-
-
1
def initialize_http_header(initheader)
-
136
@header = {}
-
136
return unless initheader
-
68
initheader.each do |key, value|
-
68
warn "net/http: duplicated HTTP header: #{key}", uplevel: 1 if key?(key) and $VERBOSE
-
68
if value.nil?
-
warn "net/http: nil HTTP header: #{key}", uplevel: 1 if $VERBOSE
-
else
-
68
value = value.strip # raise error for invalid byte sequences
-
68
if value.count("\r\n") > 0
-
raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
-
end
-
68
@header[key.downcase.to_s] = [value]
-
end
-
end
-
end
-
-
1
def size #:nodoc: obsolete
-
@header.size
-
end
-
-
1
alias length size #:nodoc: obsolete
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# For example, a key of "Content-Type" might return "text/html"
-
1
def [](key)
-
748
a = @header[key.downcase.to_s] or return nil
-
408
a.join(', ')
-
end
-
-
# Sets the header field corresponding to the case-insensitive key.
-
1
def []=(key, val)
-
204
unless val
-
@header.delete key.downcase.to_s
-
return val
-
end
-
204
set_field(key, val)
-
end
-
-
# [Ruby 1.8.3]
-
# Adds a value to a named header field, instead of replacing its value.
-
# Second argument +val+ must be a String.
-
# See also #[]=, #[] and #get_fields.
-
#
-
# request.add_field 'X-My-Header', 'a'
-
# p request['X-My-Header'] #=> "a"
-
# p request.get_fields('X-My-Header') #=> ["a"]
-
# request.add_field 'X-My-Header', 'b'
-
# p request['X-My-Header'] #=> "a, b"
-
# p request.get_fields('X-My-Header') #=> ["a", "b"]
-
# request.add_field 'X-My-Header', 'c'
-
# p request['X-My-Header'] #=> "a, b, c"
-
# p request.get_fields('X-My-Header') #=> ["a", "b", "c"]
-
#
-
1
def add_field(key, val)
-
204
stringified_downcased_key = key.downcase.to_s
-
204
if @header.key?(stringified_downcased_key)
-
append_field_value(@header[stringified_downcased_key], val)
-
else
-
204
set_field(key, val)
-
end
-
end
-
-
1
private def set_field(key, val)
-
408
case val
-
when Enumerable
-
ary = []
-
append_field_value(ary, val)
-
@header[key.downcase.to_s] = ary
-
else
-
408
val = val.to_s # for compatibility use to_s instead of to_str
-
408
if val.b.count("\r\n") > 0
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
408
@header[key.downcase.to_s] = [val]
-
end
-
end
-
-
1
private def append_field_value(ary, val)
-
case val
-
when Enumerable
-
val.each{|x| append_field_value(ary, x)}
-
else
-
val = val.to_s
-
if /[\r\n]/n.match?(val.b)
-
raise ArgumentError, 'header field value cannot include CR/LF'
-
end
-
ary.push val
-
end
-
end
-
-
# [Ruby 1.8.3]
-
# Returns an array of header field strings corresponding to the
-
# case-insensitive +key+. This method allows you to get duplicated
-
# header fields without any processing. See also #[].
-
#
-
# p response.get_fields('Set-Cookie')
-
# #=> ["session=al98axx; expires=Fri, 31-Dec-1999 23:58:23",
-
# "query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"]
-
# p response['Set-Cookie']
-
# #=> "session=al98axx; expires=Fri, 31-Dec-1999 23:58:23, query=rubyscript; expires=Fri, 31-Dec-1999 23:58:23"
-
#
-
1
def get_fields(key)
-
stringified_downcased_key = key.downcase.to_s
-
return nil unless @header[stringified_downcased_key]
-
@header[stringified_downcased_key].dup
-
end
-
-
# Returns the header field corresponding to the case-insensitive key.
-
# Returns the default value +args+, or the result of the block, or
-
# raises an IndexError if there's no header field named +key+
-
# See Hash#fetch
-
1
def fetch(key, *args, &block) #:yield: +key+
-
a = @header.fetch(key.downcase.to_s, *args, &block)
-
a.kind_of?(Array) ? a.join(', ') : a
-
end
-
-
# Iterates through the header names and values, passing in the name
-
# and value to the code block supplied.
-
#
-
# Returns an enumerator if no block is given.
-
#
-
# Example:
-
#
-
# response.header.each_header {|key,value| puts "#{key} = #{value}" }
-
#
-
1
def each_header #:yield: +key+, +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each do |k,va|
-
yield k, va.join(', ')
-
end
-
end
-
-
1
alias each each_header
-
-
# Iterates through the header names in the header, passing
-
# each header name to the code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_name(&block) #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key(&block)
-
end
-
-
1
alias each_key each_name
-
-
# Iterates through the header names in the header, passing
-
# capitalized header names to the code block.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized_name #:yield: +key+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_key do |k|
-
yield capitalize(k)
-
end
-
end
-
-
# Iterates through header values, passing each value to the
-
# code block.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_value #:yield: +value+
-
block_given? or return enum_for(__method__) { @header.size }
-
@header.each_value do |va|
-
yield va.join(', ')
-
end
-
end
-
-
# Removes a header field, specified by case-insensitive key.
-
1
def delete(key)
-
68
@header.delete(key.downcase.to_s)
-
end
-
-
# true if +key+ header exists.
-
1
def key?(key)
-
136
@header.key?(key.downcase.to_s)
-
end
-
-
# Returns a Hash consisting of header names and array of values.
-
# e.g.
-
# {"cache-control" => ["private"],
-
# "content-type" => ["text/html"],
-
# "date" => ["Wed, 22 Jun 2005 22:11:50 GMT"]}
-
1
def to_hash
-
@header.dup
-
end
-
-
# As for #each_header, except the keys are provided in capitalized form.
-
#
-
# Note that header names are capitalized systematically;
-
# capitalization may not match that used by the remote HTTP
-
# server in its response.
-
#
-
# Returns an enumerator if no block is given.
-
1
def each_capitalized
-
68
block_given? or return enum_for(__method__) { @header.size }
-
68
@header.each do |k,v|
-
408
yield capitalize(k), v.join(', ')
-
end
-
end
-
-
1
alias canonical_each each_capitalized
-
-
1
def capitalize(name)
-
1088
name.to_s.split(/-/).map {|s| s.capitalize }.join('-')
-
end
-
1
private :capitalize
-
-
# Returns an Array of Range objects which represent the Range:
-
# HTTP header field, or +nil+ if there is no such header.
-
1
def range
-
return nil unless @header['range']
-
-
value = self['Range']
-
# byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
-
# *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
-
# corrected collected ABNF
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
-
# http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
-
unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
-
raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
-
end
-
-
byte_range_set = $1
-
result = byte_range_set.split(/,/).map {|spec|
-
m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
-
raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
-
d1 = m[1].to_i
-
d2 = m[2].to_i
-
if m[1] and m[2]
-
if d1 > d2
-
raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
-
end
-
d1..d2
-
elsif m[1]
-
d1..-1
-
elsif m[2]
-
-d2..-1
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range is not specified'
-
end
-
}
-
# if result.empty?
-
# byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
-
# but above regexp already denies it.
-
if result.size == 1 && result[0].begin == 0 && result[0].end == -1
-
raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
-
end
-
result
-
end
-
-
# Sets the HTTP Range: header.
-
# Accepts either a Range object as a single argument,
-
# or a beginning index and a length from that index.
-
# Example:
-
#
-
# req.range = (0..1023)
-
# req.set_range 0, 1023
-
#
-
1
def set_range(r, e = nil)
-
unless r
-
@header.delete 'range'
-
return r
-
end
-
r = (r...r+e) if e
-
case r
-
when Numeric
-
n = r.to_i
-
rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
-
when Range
-
first = r.first
-
last = r.end
-
last -= 1 if r.exclude_end?
-
if last == -1
-
rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
-
else
-
raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
-
raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
-
raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
-
rangestr = "#{first}-#{last}"
-
end
-
else
-
raise TypeError, 'Range/Integer is required'
-
end
-
@header['range'] = ["bytes=#{rangestr}"]
-
r
-
end
-
-
1
alias range= set_range
-
-
# Returns an Integer object which represents the HTTP Content-Length:
-
# header field, or +nil+ if that field was not provided.
-
1
def content_length
-
68
return nil unless key?('Content-Length')
-
len = self['Content-Length'].slice(/\d+/) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
-
len.to_i
-
end
-
-
1
def content_length=(len)
-
68
unless len
-
@header.delete 'content-length'
-
return nil
-
end
-
68
@header['content-length'] = [len.to_i.to_s]
-
end
-
-
# Returns "true" if the "transfer-encoding" header is present and
-
# set to "chunked". This is an HTTP/1.1 feature, allowing the
-
# the content to be sent in "chunks" without at the outset
-
# stating the entire content length.
-
1
def chunked?
-
68
return false unless @header['transfer-encoding']
-
field = self['Transfer-Encoding']
-
(/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
-
end
-
-
# Returns a Range object which represents the value of the Content-Range:
-
# header field.
-
# For a partial entity body, this indicates where this fragment
-
# fits inside the full entity body, as range of byte offsets.
-
1
def content_range
-
68
return nil unless @header['content-range']
-
m = %r<bytes\s+(\d+)-(\d+)/(\d+|\*)>i.match(self['Content-Range']) or
-
raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
-
m[1].to_i .. m[2].to_i
-
end
-
-
# The length of the range represented in Content-Range: header.
-
1
def range_length
-
68
r = content_range() or return nil
-
r.end - r.begin + 1
-
end
-
-
# Returns a content type string such as "text/html".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def content_type
-
68
return nil unless main_type()
-
68
if sub_type()
-
68
then "#{main_type()}/#{sub_type()}"
-
else main_type()
-
end
-
end
-
-
# Returns a content type string such as "text".
-
# This method returns nil if Content-Type: header field does not exist.
-
1
def main_type
-
136
return nil unless @header['content-type']
-
136
self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
-
end
-
-
# Returns a content type string such as "html".
-
# This method returns nil if Content-Type: header field does not exist
-
# or sub-type is not given (e.g. "Content-Type: text").
-
1
def sub_type
-
136
return nil unless @header['content-type']
-
136
_, sub = *self['Content-Type'].split(';').first.to_s.split('/')
-
136
return nil unless sub
-
136
sub.strip
-
end
-
-
# Any parameters specified for the content type, returned as a Hash.
-
# For example, a header of Content-Type: text/html; charset=EUC-JP
-
# would result in type_params returning {'charset' => 'EUC-JP'}
-
1
def type_params
-
result = {}
-
list = self['Content-Type'].to_s.split(';')
-
list.shift
-
list.each do |param|
-
k, v = *param.split('=', 2)
-
result[k.strip] = v.strip
-
end
-
result
-
end
-
-
# Sets the content type in an HTTP header.
-
# The +type+ should be a full HTTP content type, e.g. "text/html".
-
# The +params+ are an optional Hash of parameters to add after the
-
# content type, e.g. {'charset' => 'iso-8859-1'}
-
1
def set_content_type(type, params = {})
-
68
@header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
-
end
-
-
1
alias content_type= set_content_type
-
-
# Set header fields and a body from HTML form data.
-
# +params+ should be an Array of Arrays or
-
# a Hash containing HTML form data.
-
# Optional argument +sep+ means data record separator.
-
#
-
# Values are URL encoded as necessary and the content-type is set to
-
# application/x-www-form-urlencoded
-
#
-
# Example:
-
# http.form_data = {"q" => "ruby", "lang" => "en"}
-
# http.form_data = {"q" => ["ruby", "perl"], "lang" => "en"}
-
# http.set_form_data({"q" => "ruby", "lang" => "en"}, ';')
-
#
-
1
def set_form_data(params, sep = '&')
-
query = URI.encode_www_form(params)
-
query.gsub!(/&/, sep) if sep != '&'
-
self.body = query
-
self.content_type = 'application/x-www-form-urlencoded'
-
end
-
-
1
alias form_data= set_form_data
-
-
# Set an HTML form data set.
-
# +params+ is the form data set; it is an Array of Arrays or a Hash
-
# +enctype is the type to encode the form data set.
-
# It is application/x-www-form-urlencoded or multipart/form-data.
-
# +formopt+ is an optional hash to specify the detail.
-
#
-
# boundary:: the boundary of the multipart message
-
# charset:: the charset of the message. All names and the values of
-
# non-file fields are encoded as the charset.
-
#
-
# Each item of params is an array and contains following items:
-
# +name+:: the name of the field
-
# +value+:: the value of the field, it should be a String or a File
-
# +opt+:: an optional hash to specify additional information
-
#
-
# Each item is a file field or a normal field.
-
# If +value+ is a File object or the +opt+ have a filename key,
-
# the item is treated as a file field.
-
#
-
# If Transfer-Encoding is set as chunked, this send the request in
-
# chunked encoding. Because chunked encoding is HTTP/1.1 feature,
-
# you must confirm the server to support HTTP/1.1 before sending it.
-
#
-
# Example:
-
# http.set_form([["q", "ruby"], ["lang", "en"]])
-
#
-
# See also RFC 2388, RFC 2616, HTML 4.01, and HTML5
-
#
-
1
def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
-
@body_data = params
-
@body = nil
-
@body_stream = nil
-
@form_option = formopt
-
case enctype
-
when /\Aapplication\/x-www-form-urlencoded\z/i,
-
/\Amultipart\/form-data\z/i
-
self.content_type = enctype
-
else
-
raise ArgumentError, "invalid enctype: #{enctype}"
-
end
-
end
-
-
# Set the Authorization: header for "Basic" authorization.
-
1
def basic_auth(account, password)
-
@header['authorization'] = [basic_encode(account, password)]
-
end
-
-
# Set Proxy-Authorization: header for "Basic" authorization.
-
1
def proxy_basic_auth(account, password)
-
@header['proxy-authorization'] = [basic_encode(account, password)]
-
end
-
-
1
def basic_encode(account, password)
-
'Basic ' + ["#{account}:#{password}"].pack('m0')
-
end
-
1
private :basic_encode
-
-
1
def connection_close?
-
136
token = /(?:\A|,)\s*close\s*(?:\z|,)/i
-
204
@header['connection']&.grep(token) {return true}
-
68
@header['proxy-connection']&.grep(token) {return true}
-
68
false
-
end
-
-
1
def connection_keep_alive?
-
token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
-
@header['connection']&.grep(token) {return true}
-
@header['proxy-connection']&.grep(token) {return true}
-
false
-
end
-
-
end
-
# frozen_string_literal: false
-
1
module Net::HTTP::ProxyDelta #:nodoc: internal use only
-
1
private
-
-
1
def conn_address
-
proxy_address()
-
end
-
-
1
def conn_port
-
proxy_port()
-
end
-
-
1
def edit_path(path)
-
use_ssl? ? path : "http://#{addr_port()}#{path}"
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP request class.
-
# This class wraps together the request header and the request path.
-
# You cannot use this class directly. Instead, you should use one of its
-
# subclasses: Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Head.
-
#
-
1
class Net::HTTPRequest < Net::HTTPGenericRequest
-
# Creates an HTTP request object for +path+.
-
#
-
# +initheader+ are the default headers to use. Net::HTTP adds
-
# Accept-Encoding to enable compression of the response body unless
-
# Accept-Encoding or Range are supplied in +initheader+.
-
-
1
def initialize(path, initheader = nil)
-
68
super self.class::METHOD,
-
self.class::REQUEST_HAS_BODY,
-
self.class::RESPONSE_HAS_BODY,
-
path, initheader
-
end
-
end
-
-
# frozen_string_literal: false
-
# HTTP response class.
-
#
-
# This class wraps together the response header and the response body (the
-
# entity requested).
-
#
-
# It mixes in the HTTPHeader module, which provides access to response
-
# header values both via hash-like methods and via individual readers.
-
#
-
# Note that each possible HTTP response code defines its own
-
# HTTPResponse subclass. These are listed below.
-
#
-
# All classes are defined under the Net module. Indentation indicates
-
# inheritance. For a list of the classes see Net::HTTP.
-
#
-
#
-
1
class Net::HTTPResponse
-
1
class << self
-
# true if the response has a body.
-
1
def body_permitted?
-
68
self::HAS_BODY
-
end
-
-
1
def exception_type # :nodoc: internal use only
-
self::EXCEPTION_TYPE
-
end
-
-
1
def read_new(sock) #:nodoc: internal use only
-
68
httpv, code, msg = read_status_line(sock)
-
68
res = response_class(code).new(httpv, code, msg)
-
68
each_response_header(sock) do |k,v|
-
204
res.add_field k, v
-
end
-
68
res
-
end
-
-
1
private
-
-
1
def read_status_line(sock)
-
68
str = sock.readline
-
68
m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
-
raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
-
68
m.captures
-
end
-
-
1
def response_class(code)
-
68
CODE_TO_OBJ[code] or
-
CODE_CLASS_TO_OBJ[code[0,1]] or
-
Net::HTTPUnknownResponse
-
end
-
-
1
def each_response_header(sock)
-
68
key = value = nil
-
68
while true
-
272
line = sock.readuntil("\n", true).sub(/\s+\z/, '')
-
272
break if line.empty?
-
204
if line[0] == ?\s or line[0] == ?\t and value
-
value << ' ' unless value.empty?
-
value << line.strip
-
else
-
204
yield key, value if key
-
204
key, value = line.strip.split(/\s*:\s*/, 2)
-
204
raise Net::HTTPBadResponse, 'wrong header line format' if value.nil?
-
end
-
end
-
68
yield key, value if key
-
end
-
end
-
-
# next is to fix bug in RDoc, where the private inside class << self
-
# spills out.
-
1
public
-
-
1
include Net::HTTPHeader
-
-
1
def initialize(httpv, code, msg) #:nodoc: internal use only
-
68
@http_version = httpv
-
68
@code = code
-
68
@message = msg
-
68
initialize_http_header nil
-
68
@body = nil
-
68
@read = false
-
68
@uri = nil
-
68
@decode_content = false
-
end
-
-
# The HTTP version supported by the server.
-
1
attr_reader :http_version
-
-
# The HTTP result code string. For example, '302'. You can also
-
# determine the response type by examining which response subclass
-
# the response object is an instance of.
-
1
attr_reader :code
-
-
# The HTTP result message sent by the server. For example, 'Not Found'.
-
1
attr_reader :message
-
1
alias msg message # :nodoc: obsolete
-
-
# The URI used to fetch this response. The response URI is only available
-
# if a URI was used to create the request.
-
1
attr_reader :uri
-
-
# Set to true automatically when the request did not contain an
-
# Accept-Encoding header from the user.
-
1
attr_accessor :decode_content
-
-
1
def inspect
-
"#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
-
end
-
-
#
-
# response <-> exception relationship
-
#
-
-
1
def code_type #:nodoc:
-
self.class
-
end
-
-
1
def error! #:nodoc:
-
message = @code
-
message += ' ' + @message.dump if @message
-
raise error_type().new(message, self)
-
end
-
-
1
def error_type #:nodoc:
-
self.class::EXCEPTION_TYPE
-
end
-
-
# Raises an HTTP error if the response is not 2xx (success).
-
1
def value
-
error! unless self.kind_of?(Net::HTTPSuccess)
-
end
-
-
1
def uri= uri # :nodoc:
-
68
@uri = uri.dup if uri
-
end
-
-
#
-
# header (for backward compatibility only; DO NOT USE)
-
#
-
-
1
def response #:nodoc:
-
warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def header #:nodoc:
-
warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
1
def read_header #:nodoc:
-
warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
-
self
-
end
-
-
#
-
# body
-
#
-
-
1
def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
-
68
@socket = sock
-
68
@body_exist = reqmethodallowbody && self.class.body_permitted?
-
begin
-
68
yield
-
68
self.body # ensure to read body
-
ensure
-
68
@socket = nil
-
end
-
end
-
-
# Gets the entity body returned by the remote HTTP server.
-
#
-
# If a block is given, the body is passed to the block, and
-
# the body is provided in fragments, as it is read in from the socket.
-
#
-
# Calling this method a second or subsequent time for the same
-
# HTTPResponse object will return the value already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.read_body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.read_body.object_id # 538149362
-
# p res.read_body.object_id # 538149362
-
# }
-
#
-
# # using iterator
-
# http.request_get('/index.html') {|res|
-
# res.read_body do |segment|
-
# print segment
-
# end
-
# }
-
#
-
1
def read_body(dest = nil, &block)
-
204
if @read
-
136
raise IOError, "#{self.class}\#read_body called twice" if dest or block
-
136
return @body
-
end
-
68
to = procdest(dest, block)
-
68
stream_check
-
68
if @body_exist
-
68
read_body_0 to
-
68
@body = to
-
else
-
@body = nil
-
end
-
68
@read = true
-
-
68
@body
-
end
-
-
# Returns the full entity body.
-
#
-
# Calling this method a second or subsequent time will return the
-
# string already read.
-
#
-
# http.request_get('/index.html') {|res|
-
# puts res.body
-
# }
-
#
-
# http.request_get('/index.html') {|res|
-
# p res.body.object_id # 538149362
-
# p res.body.object_id # 538149362
-
# }
-
#
-
1
def body
-
204
read_body()
-
end
-
-
# Because it may be necessary to modify the body, Eg, decompression
-
# this method facilitates that.
-
1
def body=(value)
-
@body = value
-
end
-
-
1
alias entity body #:nodoc: obsolete
-
-
1
private
-
-
##
-
# Checks for a supported Content-Encoding header and yields an Inflate
-
# wrapper for this response's socket when zlib is present. If the
-
# Content-Encoding is not supported or zlib is missing, the plain socket is
-
# yielded.
-
#
-
# If a Content-Range header is present, a plain socket is yielded as the
-
# bytes in the range may not be a complete deflate block.
-
-
1
def inflater # :nodoc:
-
68
return yield @socket unless Net::HTTP::HAVE_ZLIB
-
68
return yield @socket unless @decode_content
-
68
return yield @socket if self['content-range']
-
-
68
v = self['content-encoding']
-
68
case v&.downcase
-
when 'deflate', 'gzip', 'x-gzip' then
-
self.delete 'content-encoding'
-
-
inflate_body_io = Inflater.new(@socket)
-
-
begin
-
yield inflate_body_io
-
ensure
-
orig_err = $!
-
begin
-
inflate_body_io.finish
-
rescue => err
-
raise orig_err || err
-
end
-
end
-
when 'none', 'identity' then
-
self.delete 'content-encoding'
-
-
yield @socket
-
else
-
68
yield @socket
-
end
-
end
-
-
1
def read_body_0(dest)
-
68
inflater do |inflate_body_io|
-
68
if chunked?
-
read_chunked dest, inflate_body_io
-
return
-
end
-
-
68
@socket = inflate_body_io
-
-
68
clen = content_length()
-
68
if clen
-
@socket.read clen, dest, true # ignore EOF
-
return
-
end
-
68
clen = range_length()
-
68
if clen
-
@socket.read clen, dest
-
return
-
end
-
68
@socket.read_all dest
-
end
-
end
-
-
##
-
# read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
-
# etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
-
# encoded.
-
#
-
# See RFC 2616 section 3.6.1 for definitions
-
-
1
def read_chunked(dest, chunk_data_io) # :nodoc:
-
total = 0
-
while true
-
line = @socket.readline
-
hexlen = line.slice(/[0-9a-fA-F]+/) or
-
raise Net::HTTPBadResponse, "wrong chunk size line: #{line}"
-
len = hexlen.hex
-
break if len == 0
-
begin
-
chunk_data_io.read len, dest
-
ensure
-
total += len
-
@socket.read 2 # \r\n
-
end
-
end
-
until @socket.readline.empty?
-
# none
-
end
-
end
-
-
1
def stream_check
-
68
raise IOError, 'attempt to read body out of block' if @socket.closed?
-
end
-
-
1
def procdest(dest, block)
-
raise ArgumentError, 'both arg and block given for HTTP method' if
-
68
dest and block
-
68
if block
-
Net::ReadAdapter.new(block)
-
else
-
68
dest || ''
-
end
-
end
-
-
##
-
# Inflater is a wrapper around Net::BufferedIO that transparently inflates
-
# zlib and gzip streams.
-
-
1
class Inflater # :nodoc:
-
-
##
-
# Creates a new Inflater wrapping +socket+
-
-
1
def initialize socket
-
@socket = socket
-
# zlib with automatic gzip detection
-
@inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
-
end
-
-
##
-
# Finishes the inflate stream.
-
-
1
def finish
-
return if @inflate.total_in == 0
-
@inflate.finish
-
end
-
-
##
-
# Returns a Net::ReadAdapter that inflates each read chunk into +dest+.
-
#
-
# This allows a large response body to be inflated without storing the
-
# entire body in memory.
-
-
1
def inflate_adapter(dest)
-
if dest.respond_to?(:set_encoding)
-
dest.set_encoding(Encoding::ASCII_8BIT)
-
elsif dest.respond_to?(:force_encoding)
-
dest.force_encoding(Encoding::ASCII_8BIT)
-
end
-
block = proc do |compressed_chunk|
-
@inflate.inflate(compressed_chunk) do |chunk|
-
compressed_chunk.clear
-
dest << chunk
-
end
-
end
-
-
Net::ReadAdapter.new(block)
-
end
-
-
##
-
# Reads +clen+ bytes from the socket, inflates them, then writes them to
-
# +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read
-
#
-
# Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes.
-
# At this time there is no way for a user of Net::HTTPResponse to read a
-
# specific number of bytes from the HTTP response body, so this internal
-
# API does not return the same number of bytes as were requested.
-
#
-
# See https://bugs.ruby-lang.org/issues/6492 for further discussion.
-
-
1
def read clen, dest, ignore_eof = false
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read clen, temp_dest, ignore_eof
-
end
-
-
##
-
# Reads the rest of the socket, inflates it, then writes it to +dest+.
-
-
1
def read_all dest
-
temp_dest = inflate_adapter(dest)
-
-
@socket.read_all temp_dest
-
end
-
-
end
-
-
end
-
-
# frozen_string_literal: true
-
# :stopdoc:
-
# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
-
1
class Net::HTTPUnknownResponse < Net::HTTPResponse
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPInformation < Net::HTTPResponse # 1xx
-
1
HAS_BODY = false
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPSuccess < Net::HTTPResponse # 2xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPError
-
end
-
1
class Net::HTTPRedirection < Net::HTTPResponse # 3xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPRetriableError
-
end
-
1
class Net::HTTPClientError < Net::HTTPResponse # 4xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPClientException # for backward compatibility
-
end
-
1
class Net::HTTPServerError < Net::HTTPResponse # 5xx
-
1
HAS_BODY = true
-
1
EXCEPTION_TYPE = Net::HTTPFatalError # for backward compatibility
-
end
-
-
1
class Net::HTTPContinue < Net::HTTPInformation # 100
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPSwitchProtocol < Net::HTTPInformation # 101
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPProcessing < Net::HTTPInformation # 102
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPEarlyHints < Net::HTTPInformation # 103 - RFC 8297
-
1
HAS_BODY = false
-
end
-
-
1
class Net::HTTPOK < Net::HTTPSuccess # 200
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPCreated < Net::HTTPSuccess # 201
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAccepted < Net::HTTPSuccess # 202
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNonAuthoritativeInformation < Net::HTTPSuccess # 203
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNoContent < Net::HTTPSuccess # 204
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPResetContent < Net::HTTPSuccess # 205
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPPartialContent < Net::HTTPSuccess # 206
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMultiStatus < Net::HTTPSuccess # 207 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPAlreadyReported < Net::HTTPSuccess # 208 - RFC 5842
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPIMUsed < Net::HTTPSuccess # 226 - RFC 3229
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPMultipleChoices < Net::HTTPRedirection # 300
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMultipleChoice = Net::HTTPMultipleChoices
-
1
class Net::HTTPMovedPermanently < Net::HTTPRedirection # 301
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFound < Net::HTTPRedirection # 302
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPMovedTemporarily = Net::HTTPFound
-
1
class Net::HTTPSeeOther < Net::HTTPRedirection # 303
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotModified < Net::HTTPRedirection # 304
-
1
HAS_BODY = false
-
end
-
1
class Net::HTTPUseProxy < Net::HTTPRedirection # 305
-
1
HAS_BODY = false
-
end
-
# 306 Switch Proxy - no longer unused
-
1
class Net::HTTPTemporaryRedirect < Net::HTTPRedirection # 307
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPermanentRedirect < Net::HTTPRedirection # 308
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPBadRequest < Net::HTTPClientError # 400
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnauthorized < Net::HTTPClientError # 401
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPaymentRequired < Net::HTTPClientError # 402
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPForbidden < Net::HTTPClientError # 403
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotFound < Net::HTTPClientError # 404
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPMethodNotAllowed < Net::HTTPClientError # 405
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotAcceptable < Net::HTTPClientError # 406
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPProxyAuthenticationRequired < Net::HTTPClientError # 407
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestTimeout < Net::HTTPClientError # 408
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestTimeOut = Net::HTTPRequestTimeout
-
1
class Net::HTTPConflict < Net::HTTPClientError # 409
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGone < Net::HTTPClientError # 410
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLengthRequired < Net::HTTPClientError # 411
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionFailed < Net::HTTPClientError # 412
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPayloadTooLarge < Net::HTTPClientError # 413
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestEntityTooLarge = Net::HTTPPayloadTooLarge
-
1
class Net::HTTPURITooLong < Net::HTTPClientError # 414
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestURITooLong = Net::HTTPURITooLong
-
1
Net::HTTPRequestURITooLarge = Net::HTTPRequestURITooLong
-
1
class Net::HTTPUnsupportedMediaType < Net::HTTPClientError # 415
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRangeNotSatisfiable < Net::HTTPClientError # 416
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPRequestedRangeNotSatisfiable = Net::HTTPRangeNotSatisfiable
-
1
class Net::HTTPExpectationFailed < Net::HTTPClientError # 417
-
1
HAS_BODY = true
-
end
-
# 418 I'm a teapot - RFC 2324; a joke RFC
-
# 420 Enhance Your Calm - Twitter
-
1
class Net::HTTPMisdirectedRequest < Net::HTTPClientError # 421 - RFC 7540
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnprocessableEntity < Net::HTTPClientError # 422 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLocked < Net::HTTPClientError # 423 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPFailedDependency < Net::HTTPClientError # 424 - RFC 4918
-
1
HAS_BODY = true
-
end
-
# 425 Unordered Collection - existed only in draft
-
1
class Net::HTTPUpgradeRequired < Net::HTTPClientError # 426 - RFC 2817
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPPreconditionRequired < Net::HTTPClientError # 428 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPTooManyRequests < Net::HTTPClientError # 429 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPRequestHeaderFieldsTooLarge < Net::HTTPClientError # 431 - RFC 6585
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPUnavailableForLegalReasons < Net::HTTPClientError # 451 - RFC 7725
-
1
HAS_BODY = true
-
end
-
# 444 No Response - Nginx
-
# 449 Retry With - Microsoft
-
# 450 Blocked by Windows Parental Controls - Microsoft
-
# 499 Client Closed Request - Nginx
-
-
1
class Net::HTTPInternalServerError < Net::HTTPServerError # 500
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNotImplemented < Net::HTTPServerError # 501
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPBadGateway < Net::HTTPServerError # 502
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPServiceUnavailable < Net::HTTPServerError # 503
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPGatewayTimeout < Net::HTTPServerError # 504
-
1
HAS_BODY = true
-
end
-
1
Net::HTTPGatewayTimeOut = Net::HTTPGatewayTimeout
-
1
class Net::HTTPVersionNotSupported < Net::HTTPServerError # 505
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPVariantAlsoNegotiates < Net::HTTPServerError # 506
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPInsufficientStorage < Net::HTTPServerError # 507 - RFC 4918
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPLoopDetected < Net::HTTPServerError # 508 - RFC 5842
-
1
HAS_BODY = true
-
end
-
# 509 Bandwidth Limit Exceeded - Apache bw/limited extension
-
1
class Net::HTTPNotExtended < Net::HTTPServerError # 510 - RFC 2774
-
1
HAS_BODY = true
-
end
-
1
class Net::HTTPNetworkAuthenticationRequired < Net::HTTPServerError # 511 - RFC 6585
-
1
HAS_BODY = true
-
end
-
-
1
class Net::HTTPResponse
-
CODE_CLASS_TO_OBJ = {
-
1
'1' => Net::HTTPInformation,
-
'2' => Net::HTTPSuccess,
-
'3' => Net::HTTPRedirection,
-
'4' => Net::HTTPClientError,
-
'5' => Net::HTTPServerError
-
}
-
CODE_TO_OBJ = {
-
1
'100' => Net::HTTPContinue,
-
'101' => Net::HTTPSwitchProtocol,
-
'102' => Net::HTTPProcessing,
-
'103' => Net::HTTPEarlyHints,
-
-
'200' => Net::HTTPOK,
-
'201' => Net::HTTPCreated,
-
'202' => Net::HTTPAccepted,
-
'203' => Net::HTTPNonAuthoritativeInformation,
-
'204' => Net::HTTPNoContent,
-
'205' => Net::HTTPResetContent,
-
'206' => Net::HTTPPartialContent,
-
'207' => Net::HTTPMultiStatus,
-
'208' => Net::HTTPAlreadyReported,
-
'226' => Net::HTTPIMUsed,
-
-
'300' => Net::HTTPMultipleChoices,
-
'301' => Net::HTTPMovedPermanently,
-
'302' => Net::HTTPFound,
-
'303' => Net::HTTPSeeOther,
-
'304' => Net::HTTPNotModified,
-
'305' => Net::HTTPUseProxy,
-
'307' => Net::HTTPTemporaryRedirect,
-
'308' => Net::HTTPPermanentRedirect,
-
-
'400' => Net::HTTPBadRequest,
-
'401' => Net::HTTPUnauthorized,
-
'402' => Net::HTTPPaymentRequired,
-
'403' => Net::HTTPForbidden,
-
'404' => Net::HTTPNotFound,
-
'405' => Net::HTTPMethodNotAllowed,
-
'406' => Net::HTTPNotAcceptable,
-
'407' => Net::HTTPProxyAuthenticationRequired,
-
'408' => Net::HTTPRequestTimeout,
-
'409' => Net::HTTPConflict,
-
'410' => Net::HTTPGone,
-
'411' => Net::HTTPLengthRequired,
-
'412' => Net::HTTPPreconditionFailed,
-
'413' => Net::HTTPPayloadTooLarge,
-
'414' => Net::HTTPURITooLong,
-
'415' => Net::HTTPUnsupportedMediaType,
-
'416' => Net::HTTPRangeNotSatisfiable,
-
'417' => Net::HTTPExpectationFailed,
-
'421' => Net::HTTPMisdirectedRequest,
-
'422' => Net::HTTPUnprocessableEntity,
-
'423' => Net::HTTPLocked,
-
'424' => Net::HTTPFailedDependency,
-
'426' => Net::HTTPUpgradeRequired,
-
'428' => Net::HTTPPreconditionRequired,
-
'429' => Net::HTTPTooManyRequests,
-
'431' => Net::HTTPRequestHeaderFieldsTooLarge,
-
'451' => Net::HTTPUnavailableForLegalReasons,
-
-
'500' => Net::HTTPInternalServerError,
-
'501' => Net::HTTPNotImplemented,
-
'502' => Net::HTTPBadGateway,
-
'503' => Net::HTTPServiceUnavailable,
-
'504' => Net::HTTPGatewayTimeout,
-
'505' => Net::HTTPVersionNotSupported,
-
'506' => Net::HTTPVariantAlsoNegotiates,
-
'507' => Net::HTTPInsufficientStorage,
-
'508' => Net::HTTPLoopDetected,
-
'510' => Net::HTTPNotExtended,
-
'511' => Net::HTTPNetworkAuthenticationRequired,
-
}
-
end
-
-
# :startdoc:
-
# frozen_string_literal: true
-
#
-
# = net/protocol.rb
-
#
-
#--
-
# Copyright (c) 1999-2004 Yukihiro Matsumoto
-
# Copyright (c) 1999-2004 Minero Aoki
-
#
-
# written and maintained by Minero Aoki <aamine@loveruby.net>
-
#
-
# This program is free software. You can re-distribute and/or
-
# modify this program under the same terms as Ruby itself,
-
# Ruby Distribute License or GNU General Public License.
-
#
-
# $Id: protocol.rb 66799 2019-01-12 21:02:26Z naruse $
-
#++
-
#
-
# WARNING: This file is going to remove.
-
# Do not rely on the implementation written in this file.
-
#
-
-
1
require 'socket'
-
1
require 'timeout'
-
1
require 'io/wait'
-
-
1
module Net # :nodoc:
-
-
1
class Protocol #:nodoc: internal use only
-
1
private
-
1
def Protocol.protocol_param(name, val)
-
module_eval(<<-End, __FILE__, __LINE__ + 1)
-
def #{name}
-
#{val}
-
end
-
End
-
end
-
-
1
def ssl_socket_connect(s, timeout)
-
if timeout
-
while true
-
raise Net::OpenTimeout if timeout <= 0
-
start = Process.clock_gettime Process::CLOCK_MONOTONIC
-
# to_io is required because SSLSocket doesn't have wait_readable yet
-
case s.connect_nonblock(exception: false)
-
when :wait_readable; s.to_io.wait_readable(timeout)
-
when :wait_writable; s.to_io.wait_writable(timeout)
-
else; break
-
end
-
timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
-
end
-
else
-
s.connect
-
end
-
end
-
end
-
-
-
1
class ProtocolError < StandardError; end
-
1
class ProtoSyntaxError < ProtocolError; end
-
1
class ProtoFatalError < ProtocolError; end
-
1
class ProtoUnknownError < ProtocolError; end
-
1
class ProtoServerError < ProtocolError; end
-
1
class ProtoAuthError < ProtocolError; end
-
1
class ProtoCommandError < ProtocolError; end
-
1
class ProtoRetriableError < ProtocolError; end
-
1
ProtocRetryError = ProtoRetriableError
-
-
##
-
# OpenTimeout, a subclass of Timeout::Error, is raised if a connection cannot
-
# be created within the open_timeout.
-
-
1
class OpenTimeout < Timeout::Error; end
-
-
##
-
# ReadTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be read within the read_timeout.
-
-
1
class ReadTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
##
-
# WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
-
# response cannot be written within the write_timeout. Not raised on Windows.
-
-
1
class WriteTimeout < Timeout::Error
-
1
def initialize(io = nil)
-
@io = io
-
end
-
1
attr_reader :io
-
-
1
def message
-
msg = super
-
if @io
-
msg = "#{msg} with #{@io.inspect}"
-
end
-
msg
-
end
-
end
-
-
-
1
class BufferedIO #:nodoc: internal use only
-
1
def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
-
68
@io = io
-
68
@read_timeout = read_timeout
-
68
@write_timeout = write_timeout
-
68
@continue_timeout = continue_timeout
-
68
@debug_output = debug_output
-
68
@rbuf = ''.b
-
end
-
-
1
attr_reader :io
-
1
attr_accessor :read_timeout
-
1
attr_accessor :write_timeout
-
1
attr_accessor :continue_timeout
-
1
attr_accessor :debug_output
-
-
1
def inspect
-
"#<#{self.class} io=#{@io}>"
-
end
-
-
1
def eof?
-
@io.eof?
-
end
-
-
1
def closed?
-
204
@io.closed?
-
end
-
-
1
def close
-
136
@io.close
-
end
-
-
#
-
# Read
-
#
-
-
1
public
-
-
1
def read(len, dest = ''.b, ignore_eof = false)
-
LOG "reading #{len} bytes..."
-
read_bytes = 0
-
begin
-
while read_bytes + @rbuf.size < len
-
s = rbuf_consume(@rbuf.size)
-
read_bytes += s.size
-
dest << s
-
rbuf_fill
-
end
-
s = rbuf_consume(len - read_bytes)
-
read_bytes += s.size
-
dest << s
-
rescue EOFError
-
raise unless ignore_eof
-
end
-
LOG "read #{read_bytes} bytes"
-
dest
-
end
-
-
1
def read_all(dest = ''.b)
-
68
LOG 'reading all...'
-
68
read_bytes = 0
-
begin
-
68
while true
-
68
s = rbuf_consume(@rbuf.size)
-
68
read_bytes += s.size
-
68
dest << s
-
68
rbuf_fill
-
end
-
rescue EOFError
-
;
-
end
-
68
LOG "read #{read_bytes} bytes"
-
68
dest
-
end
-
-
1
def readuntil(terminator, ignore_eof = false)
-
begin
-
340
until idx = @rbuf.index(terminator)
-
68
rbuf_fill
-
end
-
340
return rbuf_consume(idx + terminator.size)
-
rescue EOFError
-
raise unless ignore_eof
-
return rbuf_consume(@rbuf.size)
-
end
-
end
-
-
1
def readline
-
68
readuntil("\n").chop
-
end
-
-
1
private
-
-
1
BUFSIZE = 1024 * 16
-
-
1
def rbuf_fill
-
136
tmp = @rbuf.empty? ? @rbuf : nil
-
136
case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
-
when String
-
68
return if rv.equal?(tmp)
-
@rbuf << rv
-
rv.clear
-
return
-
when :wait_readable
-
2
(io = @io.to_io).wait_readable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when :wait_writable
-
# OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
-
# http://www.openssl.org/support/faq.html#PROG10
-
(io = @io.to_io).wait_writable(@read_timeout) or raise Net::ReadTimeout.new(io)
-
# continue looping
-
when nil
-
68
raise EOFError, 'end of file reached'
-
end while true
-
end
-
-
1
def rbuf_consume(len)
-
408
if len == @rbuf.size
-
68
s = @rbuf
-
68
@rbuf = ''.b
-
else
-
340
s = @rbuf.slice!(0, len)
-
end
-
408
@debug_output << %Q[-> #{s.dump}\n] if @debug_output
-
408
s
-
end
-
-
#
-
# Write
-
#
-
-
1
public
-
-
1
def write(*strs)
-
136
writing {
-
136
write0(*strs)
-
}
-
end
-
-
1
alias << write
-
-
1
def writeline(str)
-
writing {
-
write0 str + "\r\n"
-
}
-
end
-
-
1
private
-
-
1
def writing
-
136
@written_bytes = 0
-
136
@debug_output << '<- ' if @debug_output
-
136
yield
-
136
@debug_output << "\n" if @debug_output
-
136
bytes = @written_bytes
-
136
@written_bytes = nil
-
136
bytes
-
end
-
-
1
def write0(*strs)
-
136
@debug_output << strs.map(&:dump).join if @debug_output
-
136
orig_written_bytes = @written_bytes
-
136
strs.each_with_index do |str, i|
-
136
need_retry = true
-
136
case len = @io.write_nonblock(str, exception: false)
-
when Integer
-
136
@written_bytes += len
-
136
len -= str.bytesize
-
136
if len == 0
-
136
if strs.size == i+1
-
136
return @written_bytes - orig_written_bytes
-
else
-
need_retry = false
-
# next string
-
end
-
elsif len < 0
-
str = str.byteslice(len, -len)
-
else # len > 0
-
need_retry = false
-
# next string
-
end
-
# continue looping
-
when :wait_writable
-
(io = @io.to_io).wait_writable(@write_timeout) or raise Net::WriteTimeout.new(io)
-
# continue looping
-
end while need_retry
-
end
-
end
-
-
#
-
# Logging
-
#
-
-
1
private
-
-
1
def LOG_off
-
@save_debug_out = @debug_output
-
@debug_output = nil
-
end
-
-
1
def LOG_on
-
@debug_output = @save_debug_out
-
end
-
-
1
def LOG(msg)
-
136
return unless @debug_output
-
@debug_output << msg + "\n"
-
end
-
end
-
-
-
1
class InternetMessageIO < BufferedIO #:nodoc: internal use only
-
1
def initialize(*)
-
super
-
@wbuf = nil
-
end
-
-
#
-
# Read
-
#
-
-
1
def each_message_chunk
-
LOG 'reading message...'
-
LOG_off()
-
read_bytes = 0
-
while (line = readuntil("\r\n")) != ".\r\n"
-
read_bytes += line.size
-
yield line.delete_prefix('.')
-
end
-
LOG_on()
-
LOG "read message (#{read_bytes} bytes)"
-
end
-
-
# *library private* (cannot handle 'break')
-
1
def each_list_item
-
while (str = readuntil("\r\n")) != ".\r\n"
-
yield str.chop
-
end
-
end
-
-
1
def write_message_0(src)
-
prev = @written_bytes
-
each_crlf_line(src) do |line|
-
write0 dot_stuff(line)
-
end
-
@written_bytes - prev
-
end
-
-
#
-
# Write
-
#
-
-
1
def write_message(src)
-
LOG "writing message from #{src.class}"
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
write_message_0 src
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
def write_message_by_block(&block)
-
LOG 'writing message from block'
-
LOG_off()
-
len = writing {
-
using_each_crlf_line {
-
begin
-
block.call(WriteAdapter.new(self, :write_message_0))
-
rescue LocalJumpError
-
# allow `break' from writer block
-
end
-
}
-
}
-
LOG_on()
-
LOG "wrote #{len} bytes"
-
len
-
end
-
-
1
private
-
-
1
def dot_stuff(s)
-
s.sub(/\A\./, '..')
-
end
-
-
1
def using_each_crlf_line
-
@wbuf = ''.b
-
yield
-
if not @wbuf.empty? # unterminated last line
-
write0 dot_stuff(@wbuf.chomp) + "\r\n"
-
elsif @written_bytes == 0 # empty src
-
write0 "\r\n"
-
end
-
write0 ".\r\n"
-
@wbuf = nil
-
end
-
-
1
def each_crlf_line(src)
-
buffer_filling(@wbuf, src) do
-
while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
-
yield line.chomp("\n") + "\r\n"
-
end
-
end
-
end
-
-
1
def buffer_filling(buf, src)
-
case src
-
when String # for speeding up.
-
0.step(src.size - 1, 1024) do |i|
-
buf << src[i, 1024]
-
yield
-
end
-
when File # for speeding up.
-
while s = src.read(1024)
-
buf << s
-
yield
-
end
-
else # generic reader
-
src.each do |str|
-
buf << str
-
yield if buf.size > 1024
-
end
-
yield unless buf.empty?
-
end
-
end
-
end
-
-
-
#
-
# The writer adapter class
-
#
-
1
class WriteAdapter
-
1
def initialize(socket, method)
-
@socket = socket
-
@method_id = method
-
end
-
-
1
def inspect
-
"#<#{self.class} socket=#{@socket.inspect}>"
-
end
-
-
1
def write(str)
-
@socket.__send__(@method_id, str)
-
end
-
-
1
alias print write
-
-
1
def <<(str)
-
write str
-
self
-
end
-
-
1
def puts(str = '')
-
write str.chomp("\n") + "\n"
-
end
-
-
1
def printf(*args)
-
write sprintf(*args)
-
end
-
end
-
-
-
1
class ReadAdapter #:nodoc: internal use only
-
1
def initialize(block)
-
@block = block
-
end
-
-
1
def inspect
-
"#<#{self.class}>"
-
end
-
-
1
def <<(str)
-
call_block(str, &@block) if @block
-
end
-
-
1
private
-
-
# This method is needed because @block must be called by yield,
-
# not Proc#call. You can see difference when using `break' in
-
# the block.
-
1
def call_block(str)
-
yield str
-
end
-
end
-
-
-
1
module NetPrivate #:nodoc: obsolete
-
1
Socket = ::Net::InternetMessageIO
-
end
-
-
end # module Net
-
# frozen_string_literal: true
-
-
#
-
# = open3.rb: Popen, but with stderr, too
-
#
-
# Author:: Yukihiro Matsumoto
-
# Documentation:: Konrad Meyer
-
#
-
# Open3 gives you access to stdin, stdout, and stderr when running other
-
# programs.
-
#
-
-
#
-
# Open3 grants you access to stdin, stdout, stderr and a thread to wait for the
-
# child process when running another program.
-
# You can specify various attributes, redirections, current directory, etc., of
-
# the program in the same way as for Process.spawn.
-
#
-
# - Open3.popen3 : pipes for stdin, stdout, stderr
-
# - Open3.popen2 : pipes for stdin, stdout
-
# - Open3.popen2e : pipes for stdin, merged stdout and stderr
-
# - Open3.capture3 : give a string for stdin; get strings for stdout, stderr
-
# - Open3.capture2 : give a string for stdin; get a string for stdout
-
# - Open3.capture2e : give a string for stdin; get a string for merged stdout and stderr
-
# - Open3.pipeline_rw : pipes for first stdin and last stdout of a pipeline
-
# - Open3.pipeline_r : pipe for last stdout of a pipeline
-
# - Open3.pipeline_w : pipe for first stdin of a pipeline
-
# - Open3.pipeline_start : run a pipeline without waiting
-
# - Open3.pipeline : run a pipeline and wait for its completion
-
#
-
-
1
module Open3
-
-
# Open stdin, stdout, and stderr streams and start external executable.
-
# In addition, a thread to wait for the started process is created.
-
# The thread has a pid method and a thread variable :pid which is the pid of
-
# the started process.
-
#
-
# Block form:
-
#
-
# Open3.popen3([env,] cmd... [, opts]) {|stdin, stdout, stderr, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout, stderr, wait_thr = Open3.popen3([env,] cmd... [, opts])
-
# pid = wait_thr[:pid] # pid of the started process
-
# ...
-
# stdin.close # stdin, stdout and stderr should be closed explicitly in this form.
-
# stdout.close
-
# stderr.close
-
# exit_status = wait_thr.value # Process::Status object returned.
-
#
-
# The parameters env, cmd, and opts are passed to Process.spawn.
-
# A commandline string and a list of argument strings can be accepted as follows:
-
#
-
# Open3.popen3("echo abc") {|i, o, e, t| ... }
-
# Open3.popen3("echo", "abc") {|i, o, e, t| ... }
-
# Open3.popen3(["echo", "argv0"], "abc") {|i, o, e, t| ... }
-
#
-
# If the last parameter, opts, is a Hash, it is recognized as an option for Process.spawn.
-
#
-
# Open3.popen3("pwd", :chdir=>"/") {|i,o,e,t|
-
# p o.read.chomp #=> "/"
-
# }
-
#
-
# wait_thr.value waits for the termination of the process.
-
# The block form also waits for the process when it returns.
-
#
-
# Closing stdin, stdout and stderr does not wait for the process to complete.
-
#
-
# You should be careful to avoid deadlocks.
-
# Since pipes are fixed length buffers,
-
# Open3.popen3("prog") {|i, o, e, t| o.read } deadlocks if
-
# the program generates too much output on stderr.
-
# You should read stdout and stderr simultaneously (using threads or IO.select).
-
# However, if you don't need stderr output, you can use Open3.popen2.
-
# If merged stdout and stderr output is not a problem, you can use Open3.popen2e.
-
# If you really need stdout and stderr output as separate strings, you can consider Open3.capture3.
-
#
-
1
def popen3(*cmd, &block)
-
256
if Hash === cmd.last
-
256
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
256
in_r, in_w = IO.pipe
-
256
opts[:in] = in_r
-
256
in_w.sync = true
-
-
256
out_r, out_w = IO.pipe
-
256
opts[:out] = out_w
-
-
256
err_r, err_w = IO.pipe
-
256
opts[:err] = err_w
-
-
256
popen_run(cmd, opts, [in_r, out_w, err_w], [in_w, out_r, err_r], &block)
-
end
-
1
module_function :popen3
-
-
# Open3.popen2 is similar to Open3.popen3 except that it doesn't create a pipe for
-
# the standard error stream.
-
#
-
# Block form:
-
#
-
# Open3.popen2([env,] cmd... [, opts]) {|stdin, stdout, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout, wait_thr = Open3.popen2([env,] cmd... [, opts])
-
# ...
-
# stdin.close # stdin and stdout should be closed explicitly in this form.
-
# stdout.close
-
#
-
# See Process.spawn for the optional hash arguments _env_ and _opts_.
-
#
-
# Example:
-
#
-
# Open3.popen2("wc -c") {|i,o,t|
-
# i.print "answer to life the universe and everything"
-
# i.close
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.popen2("bc -q") {|i,o,t|
-
# i.puts "obase=13"
-
# i.puts "6 * 9"
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.popen2("dc") {|i,o,t|
-
# i.print "42P"
-
# i.close
-
# p o.read #=> "*"
-
# }
-
#
-
1
def popen2(*cmd, &block)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :popen2
-
-
# Open3.popen2e is similar to Open3.popen3 except that it merges
-
# the standard output stream and the standard error stream.
-
#
-
# Block form:
-
#
-
# Open3.popen2e([env,] cmd... [, opts]) {|stdin, stdout_and_stderr, wait_thr|
-
# pid = wait_thr.pid # pid of the started process.
-
# ...
-
# exit_status = wait_thr.value # Process::Status object returned.
-
# }
-
#
-
# Non-block form:
-
#
-
# stdin, stdout_and_stderr, wait_thr = Open3.popen2e([env,] cmd... [, opts])
-
# ...
-
# stdin.close # stdin and stdout_and_stderr should be closed explicitly in this form.
-
# stdout_and_stderr.close
-
#
-
# See Process.spawn for the optional hash arguments _env_ and _opts_.
-
#
-
# Example:
-
# # check gcc warnings
-
# source = "foo.c"
-
# Open3.popen2e("gcc", "-Wall", source) {|i,oe,t|
-
# oe.each {|line|
-
# if /warning/ =~ line
-
# ...
-
# end
-
# }
-
# }
-
#
-
1
def popen2e(*cmd, &block)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[[:out, :err]] = out_w
-
-
popen_run(cmd, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :popen2e
-
-
1
def popen_run(cmd, opts, child_io, parent_io) # :nodoc:
-
256
pid = spawn(*cmd, opts)
-
254
wait_thr = Process.detach(pid)
-
254
child_io.each(&:close)
-
254
result = [*parent_io, wait_thr]
-
254
if defined? yield
-
begin
-
254
return yield(*result)
-
ensure
-
254
parent_io.each(&:close)
-
254
wait_thr.join
-
end
-
end
-
result
-
end
-
1
module_function :popen_run
-
1
class << self
-
1
private :popen_run
-
end
-
-
# Open3.capture3 captures the standard output and the standard error of a command.
-
#
-
# stdout_str, stderr_str, status = Open3.capture3([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Examples:
-
#
-
# # dot is a command of graphviz.
-
# graph = <<'End'
-
# digraph g {
-
# a -> b
-
# }
-
# End
-
# drawn_graph, dot_log = Open3.capture3("dot -v", :stdin_data=>graph)
-
#
-
# o, e, s = Open3.capture3("echo abc; sort >&2", :stdin_data=>"foo\nbar\nbaz\n")
-
# p o #=> "abc\n"
-
# p e #=> "bar\nbaz\nfoo\n"
-
# p s #=> #<Process::Status: pid 32682 exit 0>
-
#
-
# # generate a thumbnail image using the convert command of ImageMagick.
-
# # However, if the image is really stored in a file,
-
# # system("convert", "-thumbnail", "80", "png:#{filename}", "png:-") is better
-
# # because of reduced memory consumption.
-
# # But if the image is stored in a DB or generated by the gnuplot Open3.capture2 example,
-
# # Open3.capture3 should be considered.
-
# #
-
# image = File.read("/usr/share/openclipart/png/animals/mammals/sheep-md-v0.1.png", :binmode=>true)
-
# thumbnail, err, s = Open3.capture3("convert -thumbnail 80 png:- png:-", :stdin_data=>image, :binmode=>true)
-
# if s.success?
-
# STDOUT.binmode; print thumbnail
-
# end
-
#
-
1
def capture3(*cmd)
-
256
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
256
opts = {}
-
end
-
-
256
stdin_data = opts.delete(:stdin_data) || ''
-
256
binmode = opts.delete(:binmode)
-
-
256
popen3(*cmd, opts) {|i, o, e, t|
-
254
if binmode
-
i.binmode
-
o.binmode
-
e.binmode
-
end
-
508
out_reader = Thread.new { o.read }
-
508
err_reader = Thread.new { e.read }
-
begin
-
254
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
254
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
254
i.close
-
254
[out_reader.value, err_reader.value, t.value]
-
}
-
end
-
1
module_function :capture3
-
-
# Open3.capture2 captures the standard output of a command.
-
#
-
# stdout_str, status = Open3.capture2([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Example:
-
#
-
# # factor is a command for integer factorization.
-
# o, s = Open3.capture2("factor", :stdin_data=>"42")
-
# p o #=> "42: 2 3 7\n"
-
#
-
# # generate x**2 graph in png using gnuplot.
-
# gnuplot_commands = <<"End"
-
# set terminal png
-
# plot x**2, "-" with lines
-
# 1 14
-
# 2 1
-
# 3 8
-
# 4 5
-
# e
-
# End
-
# image, s = Open3.capture2("gnuplot", :stdin_data=>gnuplot_commands, :binmode=>true)
-
#
-
1
def capture2(*cmd)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
stdin_data = opts.delete(:stdin_data)
-
binmode = opts.delete(:binmode)
-
-
popen2(*cmd, opts) {|i, o, t|
-
if binmode
-
i.binmode
-
o.binmode
-
end
-
out_reader = Thread.new { o.read }
-
if stdin_data
-
begin
-
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
end
-
i.close
-
[out_reader.value, t.value]
-
}
-
end
-
1
module_function :capture2
-
-
# Open3.capture2e captures the standard output and the standard error of a command.
-
#
-
# stdout_and_stderr_str, status = Open3.capture2e([env,] cmd... [, opts])
-
#
-
# The arguments env, cmd and opts are passed to Open3.popen3 except
-
# <code>opts[:stdin_data]</code> and <code>opts[:binmode]</code>. See Process.spawn.
-
#
-
# If <code>opts[:stdin_data]</code> is specified, it is sent to the command's standard input.
-
#
-
# If <code>opts[:binmode]</code> is true, internal pipes are set to binary mode.
-
#
-
# Example:
-
#
-
# # capture make log
-
# make_log, s = Open3.capture2e("make")
-
#
-
1
def capture2e(*cmd)
-
if Hash === cmd.last
-
opts = cmd.pop.dup
-
else
-
opts = {}
-
end
-
-
stdin_data = opts.delete(:stdin_data)
-
binmode = opts.delete(:binmode)
-
-
popen2e(*cmd, opts) {|i, oe, t|
-
if binmode
-
i.binmode
-
oe.binmode
-
end
-
outerr_reader = Thread.new { oe.read }
-
if stdin_data
-
begin
-
if stdin_data.respond_to? :readpartial
-
IO.copy_stream(stdin_data, i)
-
else
-
i.write stdin_data
-
end
-
rescue Errno::EPIPE
-
end
-
end
-
i.close
-
[outerr_reader.value, t.value]
-
}
-
end
-
1
module_function :capture2e
-
-
# Open3.pipeline_rw starts a list of commands as a pipeline with pipes
-
# which connect to stdin of the first command and stdout of the last command.
-
#
-
# Open3.pipeline_rw(cmd1, cmd2, ... [, opts]) {|first_stdin, last_stdout, wait_threads|
-
# ...
-
# }
-
#
-
# first_stdin, last_stdout, wait_threads = Open3.pipeline_rw(cmd1, cmd2, ... [, opts])
-
# ...
-
# first_stdin.close
-
# last_stdout.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# The options to pass to Process.spawn are constructed by merging
-
# +opts+, the last hash element of the array, and
-
# specifications for the pipes between each of the commands.
-
#
-
# Example:
-
#
-
# Open3.pipeline_rw("tr -dc A-Za-z", "wc -c") {|i, o, ts|
-
# i.puts "All persons more than a mile high to leave the court."
-
# i.close
-
# p o.gets #=> "42\n"
-
# }
-
#
-
# Open3.pipeline_rw("sort", "cat -n") {|stdin, stdout, wait_thrs|
-
# stdin.puts "foo"
-
# stdin.puts "bar"
-
# stdin.puts "baz"
-
# stdin.close # send EOF to sort.
-
# p stdout.read #=> " 1\tbar\n 2\tbaz\n 3\tfoo\n"
-
# }
-
1
def pipeline_rw(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
pipeline_run(cmds, opts, [in_r, out_w], [in_w, out_r], &block)
-
end
-
1
module_function :pipeline_rw
-
-
# Open3.pipeline_r starts a list of commands as a pipeline with a pipe
-
# which connects to stdout of the last command.
-
#
-
# Open3.pipeline_r(cmd1, cmd2, ... [, opts]) {|last_stdout, wait_threads|
-
# ...
-
# }
-
#
-
# last_stdout, wait_threads = Open3.pipeline_r(cmd1, cmd2, ... [, opts])
-
# ...
-
# last_stdout.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# Open3.pipeline_r("zcat /var/log/apache2/access.log.*.gz",
-
# [{"LANG"=>"C"}, "grep", "GET /favicon.ico"],
-
# "logresolve") {|o, ts|
-
# o.each_line {|line|
-
# ...
-
# }
-
# }
-
#
-
# Open3.pipeline_r("yes", "head -10") {|o, ts|
-
# p o.read #=> "y\ny\ny\ny\ny\ny\ny\ny\ny\ny\n"
-
# p ts[0].value #=> #<Process::Status: pid 24910 SIGPIPE (signal 13)>
-
# p ts[1].value #=> #<Process::Status: pid 24913 exit 0>
-
# }
-
#
-
1
def pipeline_r(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
out_r, out_w = IO.pipe
-
opts[:out] = out_w
-
-
pipeline_run(cmds, opts, [out_w], [out_r], &block)
-
end
-
1
module_function :pipeline_r
-
-
# Open3.pipeline_w starts a list of commands as a pipeline with a pipe
-
# which connects to stdin of the first command.
-
#
-
# Open3.pipeline_w(cmd1, cmd2, ... [, opts]) {|first_stdin, wait_threads|
-
# ...
-
# }
-
#
-
# first_stdin, wait_threads = Open3.pipeline_w(cmd1, cmd2, ... [, opts])
-
# ...
-
# first_stdin.close
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# Open3.pipeline_w("bzip2 -c", :out=>"/tmp/hello.bz2") {|i, ts|
-
# i.puts "hello"
-
# }
-
#
-
1
def pipeline_w(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
in_r, in_w = IO.pipe
-
opts[:in] = in_r
-
in_w.sync = true
-
-
pipeline_run(cmds, opts, [in_r], [in_w], &block)
-
end
-
1
module_function :pipeline_w
-
-
# Open3.pipeline_start starts a list of commands as a pipeline.
-
# No pipes are created for stdin of the first command and
-
# stdout of the last command.
-
#
-
# Open3.pipeline_start(cmd1, cmd2, ... [, opts]) {|wait_threads|
-
# ...
-
# }
-
#
-
# wait_threads = Open3.pipeline_start(cmd1, cmd2, ... [, opts])
-
# ...
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as for Process.spawn.
-
#
-
# Example:
-
#
-
# # Run xeyes in 10 seconds.
-
# Open3.pipeline_start("xeyes") {|ts|
-
# sleep 10
-
# t = ts[0]
-
# Process.kill("TERM", t.pid)
-
# p t.value #=> #<Process::Status: pid 911 SIGTERM (signal 15)>
-
# }
-
#
-
# # Convert pdf to ps and send it to a printer.
-
# # Collect error message of pdftops and lpr.
-
# pdf_file = "paper.pdf"
-
# printer = "printer-name"
-
# err_r, err_w = IO.pipe
-
# Open3.pipeline_start(["pdftops", pdf_file, "-"],
-
# ["lpr", "-P#{printer}"],
-
# :err=>err_w) {|ts|
-
# err_w.close
-
# p err_r.read # error messages of pdftops and lpr.
-
# }
-
#
-
1
def pipeline_start(*cmds, &block)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
if block
-
pipeline_run(cmds, opts, [], [], &block)
-
else
-
ts, = pipeline_run(cmds, opts, [], [])
-
ts
-
end
-
end
-
1
module_function :pipeline_start
-
-
# Open3.pipeline starts a list of commands as a pipeline.
-
# It waits for the completion of the commands.
-
# No pipes are created for stdin of the first command and
-
# stdout of the last command.
-
#
-
# status_list = Open3.pipeline(cmd1, cmd2, ... [, opts])
-
#
-
# Each cmd is a string or an array.
-
# If it is an array, the elements are passed to Process.spawn.
-
#
-
# cmd:
-
# commandline command line string which is passed to a shell
-
# [env, commandline, opts] command line string which is passed to a shell
-
# [env, cmdname, arg1, ..., opts] command name and one or more arguments (no shell)
-
# [env, [cmdname, argv0], arg1, ..., opts] command name and arguments including argv[0] (no shell)
-
#
-
# Note that env and opts are optional, as Process.spawn.
-
#
-
# Example:
-
#
-
# fname = "/usr/share/man/man1/ruby.1.gz"
-
# p Open3.pipeline(["zcat", fname], "nroff -man", "less")
-
# #=> [#<Process::Status: pid 11817 exit 0>,
-
# # #<Process::Status: pid 11820 exit 0>,
-
# # #<Process::Status: pid 11828 exit 0>]
-
#
-
# fname = "/usr/share/man/man1/ls.1.gz"
-
# Open3.pipeline(["zcat", fname], "nroff -man", "colcrt")
-
#
-
# # convert PDF to PS and send to a printer by lpr
-
# pdf_file = "paper.pdf"
-
# printer = "printer-name"
-
# Open3.pipeline(["pdftops", pdf_file, "-"],
-
# ["lpr", "-P#{printer}"])
-
#
-
# # count lines
-
# Open3.pipeline("sort", "uniq -c", :in=>"names.txt", :out=>"count")
-
#
-
# # cyclic pipeline
-
# r,w = IO.pipe
-
# w.print "ibase=14\n10\n"
-
# Open3.pipeline("bc", "tee /dev/tty", :in=>r, :out=>w)
-
# #=> 14
-
# # 18
-
# # 22
-
# # 30
-
# # 42
-
# # 58
-
# # 78
-
# # 106
-
# # 202
-
#
-
1
def pipeline(*cmds)
-
if Hash === cmds.last
-
opts = cmds.pop.dup
-
else
-
opts = {}
-
end
-
-
pipeline_run(cmds, opts, [], []) {|ts|
-
ts.map(&:value)
-
}
-
end
-
1
module_function :pipeline
-
-
1
def pipeline_run(cmds, pipeline_opts, child_io, parent_io) # :nodoc:
-
if cmds.empty?
-
raise ArgumentError, "no commands"
-
end
-
-
opts_base = pipeline_opts.dup
-
opts_base.delete :in
-
opts_base.delete :out
-
-
wait_thrs = []
-
r = nil
-
cmds.each_with_index {|cmd, i|
-
cmd_opts = opts_base.dup
-
if String === cmd
-
cmd = [cmd]
-
else
-
cmd_opts.update cmd.pop if Hash === cmd.last
-
end
-
if i == 0
-
if !cmd_opts.include?(:in)
-
if pipeline_opts.include?(:in)
-
cmd_opts[:in] = pipeline_opts[:in]
-
end
-
end
-
else
-
cmd_opts[:in] = r
-
end
-
if i != cmds.length - 1
-
r2, w2 = IO.pipe
-
cmd_opts[:out] = w2
-
else
-
if !cmd_opts.include?(:out)
-
if pipeline_opts.include?(:out)
-
cmd_opts[:out] = pipeline_opts[:out]
-
end
-
end
-
end
-
pid = spawn(*cmd, cmd_opts)
-
wait_thrs << Process.detach(pid)
-
r&.close
-
w2&.close
-
r = r2
-
}
-
result = parent_io + [wait_thrs]
-
child_io.each(&:close)
-
if defined? yield
-
begin
-
return yield(*result)
-
ensure
-
parent_io.each(&:close)
-
wait_thrs.each(&:join)
-
end
-
end
-
result
-
end
-
1
module_function :pipeline_run
-
1
class << self
-
1
private :pipeline_run
-
end
-
-
end
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require 'openssl.so'
-
-
1
require 'openssl/bn'
-
1
require 'openssl/pkey'
-
1
require 'openssl/cipher'
-
1
require 'openssl/config'
-
1
require 'openssl/digest'
-
1
require 'openssl/x509'
-
1
require 'openssl/ssl'
-
1
require 'openssl/pkcs5'
-
# frozen_string_literal: false
-
#--
-
#
-
# = Ruby-space definitions that completes C-space funcs for BN
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class BN
-
1
include Comparable
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_i.to_s
-
}
-
end
-
end # BN
-
end # OpenSSL
-
-
##
-
#--
-
# Add double dispatch to Integer
-
#++
-
1
class Integer
-
# Casts an Integer as an OpenSSL::BN
-
#
-
# See `man bn` for more info.
-
1
def to_bn
-
OpenSSL::BN::new(self)
-
end
-
end # Integer
-
# coding: binary
-
# frozen_string_literal: false
-
#--
-
#= Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
# All rights reserved.
-
#
-
#= Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
##
-
# OpenSSL IO buffering mix-in module.
-
#
-
# This module allows an OpenSSL::SSL::SSLSocket to behave like an IO.
-
#
-
# You typically won't use this module directly, you can see it implemented in
-
# OpenSSL::SSL::SSLSocket.
-
-
1
module OpenSSL::Buffering
-
1
include Enumerable
-
-
##
-
# The "sync mode" of the SSLSocket.
-
#
-
# See IO#sync for full details.
-
-
1
attr_accessor :sync
-
-
##
-
# Default size to read from or write to the SSLSocket for buffer operations.
-
-
1
BLOCK_SIZE = 1024*16
-
-
##
-
# Creates an instance of OpenSSL's buffering IO module.
-
-
1
def initialize(*)
-
super
-
@eof = false
-
@rbuffer = ""
-
@sync = @io.sync
-
end
-
-
#
-
# for reading.
-
#
-
1
private
-
-
##
-
# Fills the buffer from the underlying SSLSocket
-
-
1
def fill_rbuff
-
begin
-
@rbuffer << self.sysread(BLOCK_SIZE)
-
rescue Errno::EAGAIN
-
retry
-
rescue EOFError
-
@eof = true
-
end
-
end
-
-
##
-
# Consumes _size_ bytes from the buffer
-
-
1
def consume_rbuff(size=nil)
-
if @rbuffer.empty?
-
nil
-
else
-
size = @rbuffer.size unless size
-
ret = @rbuffer[0, size]
-
@rbuffer[0, size] = ""
-
ret
-
end
-
end
-
-
1
public
-
-
##
-
# Reads _size_ bytes from the stream. If _buf_ is provided it must
-
# reference a string which will receive the data.
-
#
-
# See IO#read for full details.
-
-
1
def read(size=nil, buf=nil)
-
if size == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
until @eof
-
break if size && size <= @rbuffer.size
-
fill_rbuff
-
end
-
ret = consume_rbuff(size) || ""
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
(size && ret.empty?) ? nil : ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes from the stream. If _buf_ is provided it
-
# must reference a string which will receive the data.
-
#
-
# See IO#readpartial for full details.
-
-
1
def readpartial(maxlen, buf=nil)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
begin
-
return sysread(maxlen, buf)
-
rescue Errno::EAGAIN
-
retry
-
end
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads at most _maxlen_ bytes in the non-blocking manner.
-
#
-
# When no data can be read without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so read_nonblock
-
# should be called again when the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so read_nonblock
-
# should be called again after the underlying IO is writable.
-
#
-
# OpenSSL::Buffering#read_nonblock needs two rescue clause as follows:
-
#
-
# # emulates blocking read (readpartial).
-
# begin
-
# result = ssl.read_nonblock(maxlen)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that read_nonblock writes to the underlying IO is
-
# when the peer requests a new TLS/SSL handshake. See openssl the FAQ for
-
# more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that read_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead. At EOF,
-
# it will return +nil+ instead of raising EOFError.
-
-
1
def read_nonblock(maxlen, buf=nil, exception: true)
-
if maxlen == 0
-
if buf
-
buf.clear
-
return buf
-
else
-
return ""
-
end
-
end
-
if @rbuffer.empty?
-
return sysread_nonblock(maxlen, buf, exception: exception)
-
end
-
ret = consume_rbuff(maxlen)
-
if buf
-
buf.replace(ret)
-
ret = buf
-
end
-
ret
-
end
-
-
##
-
# Reads the next "line" from the stream. Lines are separated by _eol_. If
-
# _limit_ is provided the result will not be longer than the given number of
-
# bytes.
-
#
-
# _eol_ may be a String or Regexp.
-
#
-
# Unlike IO#gets the line read will not be assigned to +$_+.
-
#
-
# Unlike IO#gets the separator must be provided if a limit is provided.
-
-
1
def gets(eol=$/, limit=nil)
-
idx = @rbuffer.index(eol)
-
until @eof
-
break if idx
-
fill_rbuff
-
idx = @rbuffer.index(eol)
-
end
-
if eol.is_a?(Regexp)
-
size = idx ? idx+$&.size : nil
-
else
-
size = idx ? idx+eol.size : nil
-
end
-
if size && limit && limit >= 0
-
size = [size, limit].min
-
end
-
consume_rbuff(size)
-
end
-
-
##
-
# Executes the block for every line in the stream where lines are separated
-
# by _eol_.
-
#
-
# See also #gets
-
-
1
def each(eol=$/)
-
while line = self.gets(eol)
-
yield line
-
end
-
end
-
1
alias each_line each
-
-
##
-
# Reads lines from the stream which are separated by _eol_.
-
#
-
# See also #gets
-
-
1
def readlines(eol=$/)
-
ary = []
-
while line = self.gets(eol)
-
ary << line
-
end
-
ary
-
end
-
-
##
-
# Reads a line from the stream which is separated by _eol_.
-
#
-
# Raises EOFError if at end of file.
-
-
1
def readline(eol=$/)
-
raise EOFError if eof?
-
gets(eol)
-
end
-
-
##
-
# Reads one character from the stream. Returns nil if called at end of
-
# file.
-
-
1
def getc
-
read(1)
-
end
-
-
##
-
# Calls the given block once for each byte in the stream.
-
-
1
def each_byte # :yields: byte
-
while c = getc
-
yield(c.ord)
-
end
-
end
-
-
##
-
# Reads a one-character string from the stream. Raises an EOFError at end
-
# of file.
-
-
1
def readchar
-
raise EOFError if eof?
-
getc
-
end
-
-
##
-
# Pushes character _c_ back onto the stream such that a subsequent buffered
-
# character read will return it.
-
#
-
# Unlike IO#getc multiple bytes may be pushed back onto the stream.
-
#
-
# Has no effect on unbuffered reads (such as #sysread).
-
-
1
def ungetc(c)
-
@rbuffer[0,0] = c.chr
-
end
-
-
##
-
# Returns true if the stream is at file which means there is no more data to
-
# be read.
-
-
1
def eof?
-
fill_rbuff if !@eof && @rbuffer.empty?
-
@eof && @rbuffer.empty?
-
end
-
1
alias eof eof?
-
-
#
-
# for writing.
-
#
-
1
private
-
-
##
-
# Writes _s_ to the buffer. When the buffer is full or #sync is true the
-
# buffer is flushed to the underlying socket.
-
-
1
def do_write(s)
-
@wbuffer = "" unless defined? @wbuffer
-
@wbuffer << s
-
@wbuffer.force_encoding(Encoding::BINARY)
-
@sync ||= false
-
if @sync or @wbuffer.size > BLOCK_SIZE
-
until @wbuffer.empty?
-
begin
-
nwrote = syswrite(@wbuffer)
-
rescue Errno::EAGAIN
-
retry
-
end
-
@wbuffer[0, nwrote] = ""
-
end
-
end
-
end
-
-
1
public
-
-
##
-
# Writes _s_ to the stream. If the argument is not a String it will be
-
# converted using +.to_s+ method. Returns the number of bytes written.
-
-
1
def write(*s)
-
s.inject(0) do |written, str|
-
do_write(str)
-
written + str.bytesize
-
end
-
end
-
-
##
-
# Writes _s_ in the non-blocking manner.
-
#
-
# If there is buffered data, it is flushed first. This may block.
-
#
-
# write_nonblock returns number of bytes written to the SSL connection.
-
#
-
# When no data can be written without blocking it raises
-
# OpenSSL::SSL::SSLError extended by IO::WaitReadable or IO::WaitWritable.
-
#
-
# IO::WaitReadable means SSL needs to read internally so write_nonblock
-
# should be called again after the underlying IO is readable.
-
#
-
# IO::WaitWritable means SSL needs to write internally so write_nonblock
-
# should be called again after underlying IO is writable.
-
#
-
# So OpenSSL::Buffering#write_nonblock needs two rescue clause as follows.
-
#
-
# # emulates blocking write.
-
# begin
-
# result = ssl.write_nonblock(str)
-
# rescue IO::WaitReadable
-
# IO.select([io])
-
# retry
-
# rescue IO::WaitWritable
-
# IO.select(nil, [io])
-
# retry
-
# end
-
#
-
# Note that one reason that write_nonblock reads from the underlying IO
-
# is when the peer requests a new TLS/SSL handshake. See the openssl FAQ
-
# for more details. http://www.openssl.org/support/faq.html
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that write_nonblock should not raise an IO::Wait*able exception, but
-
# return the symbol +:wait_writable+ or +:wait_readable+ instead.
-
-
1
def write_nonblock(s, exception: true)
-
flush
-
syswrite_nonblock(s, exception: exception)
-
end
-
-
##
-
# Writes _s_ to the stream. _s_ will be converted to a String using
-
# +.to_s+ method.
-
-
1
def <<(s)
-
do_write(s)
-
self
-
end
-
-
##
-
# Writes _args_ to the stream along with a record separator.
-
#
-
# See IO#puts for full details.
-
-
1
def puts(*args)
-
s = ""
-
if args.empty?
-
s << "\n"
-
end
-
args.each{|arg|
-
s << arg.to_s
-
s.sub!(/(?<!\n)\z/, "\n")
-
}
-
do_write(s)
-
nil
-
end
-
-
##
-
# Writes _args_ to the stream.
-
#
-
# See IO#print for full details.
-
-
1
def print(*args)
-
s = ""
-
args.each{ |arg| s << arg.to_s }
-
do_write(s)
-
nil
-
end
-
-
##
-
# Formats and writes to the stream converting parameters under control of
-
# the format string.
-
#
-
# See Kernel#sprintf for format string details.
-
-
1
def printf(s, *args)
-
do_write(s % args)
-
nil
-
end
-
-
##
-
# Flushes buffered data to the SSLSocket.
-
-
1
def flush
-
osync = @sync
-
@sync = true
-
do_write ""
-
return self
-
ensure
-
@sync = osync
-
end
-
-
##
-
# Closes the SSLSocket and flushes any unwritten data.
-
-
1
def close
-
flush rescue nil
-
sysclose
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Cipher subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Cipher
-
1
%w(AES CAST5 BF DES IDEA RC2 RC4 RC5).each{|name|
-
8
klass = Class.new(Cipher){
-
8
define_method(:initialize){|*args|
-
cipher_name = args.inject(name){|n, arg| "#{n}-#{arg}" }
-
super(cipher_name.downcase)
-
}
-
}
-
8
const_set(name, klass)
-
}
-
-
1
%w(128 192 256).each{|keylen|
-
3
klass = Class.new(Cipher){
-
3
define_method(:initialize){|mode = "CBC"|
-
super("aes-#{keylen}-#{mode}".downcase)
-
}
-
}
-
3
const_set("AES#{keylen}", klass)
-
}
-
-
# call-seq:
-
# cipher.random_key -> key
-
#
-
# Generate a random key with OpenSSL::Random.random_bytes and sets it to
-
# the cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_key
-
str = OpenSSL::Random.random_bytes(self.key_len)
-
self.key = str
-
end
-
-
# call-seq:
-
# cipher.random_iv -> iv
-
#
-
# Generate a random IV with OpenSSL::Random.random_bytes and sets it to the
-
# cipher, and returns it.
-
#
-
# You must call #encrypt or #decrypt before calling this method.
-
1
def random_iv
-
str = OpenSSL::Random.random_bytes(self.iv_len)
-
self.iv = str
-
end
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Cipher.
-
1
class Cipher < Cipher; end
-
1
deprecate_constant :Cipher
-
end # Cipher
-
end # OpenSSL
-
# frozen_string_literal: false
-
=begin
-
= Ruby-space definitions that completes C-space funcs for Config
-
-
= Info
-
Copyright (C) 2010 Hiroshi Nakamura <nahi@ruby-lang.org>
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
-
=end
-
-
1
require 'stringio'
-
-
1
module OpenSSL
-
##
-
# = OpenSSL::Config
-
#
-
# Configuration for the openssl library.
-
#
-
# Many system's installation of openssl library will depend on your system
-
# configuration. See the value of OpenSSL::Config::DEFAULT_CONFIG_FILE for
-
# the location of the file for your host.
-
#
-
# See also http://www.openssl.org/docs/apps/config.html
-
1
class Config
-
1
include Enumerable
-
-
1
class << self
-
-
##
-
# Parses a given _string_ as a blob that contains configuration for
-
# OpenSSL.
-
#
-
# If the source of the IO is a file, then consider using #parse_config.
-
1
def parse(string)
-
c = new()
-
parse_config(StringIO.new(string)).each do |section, hash|
-
c[section] = hash
-
end
-
c
-
end
-
-
##
-
# load is an alias to ::new
-
1
alias load new
-
-
##
-
# Parses the configuration data read from _io_, see also #parse.
-
#
-
# Raises a ConfigError on invalid configuration data.
-
1
def parse_config(io)
-
begin
-
parse_config_lines(io)
-
rescue ConfigError => e
-
e.message.replace("error in line #{io.lineno}: " + e.message)
-
raise
-
end
-
end
-
-
1
def get_key_string(data, section, key) # :nodoc:
-
if v = data[section] && data[section][key]
-
return v
-
elsif section == 'ENV'
-
if v = ENV[key]
-
return v
-
end
-
end
-
if v = data['default'] && data['default'][key]
-
return v
-
end
-
end
-
-
1
private
-
-
1
def parse_config_lines(io)
-
section = 'default'
-
data = {section => {}}
-
io_stack = [io]
-
while definition = get_definition(io_stack)
-
definition = clear_comments(definition)
-
next if definition.empty?
-
case definition
-
when /\A\[/
-
if /\[([^\]]*)\]/ =~ definition
-
section = $1.strip
-
data[section] ||= {}
-
else
-
raise ConfigError, "missing close square bracket"
-
end
-
when /\A\.include (\s*=\s*)?(.+)\z/
-
path = $2
-
if File.directory?(path)
-
files = Dir.glob(File.join(path, "*.{cnf,conf}"), File::FNM_EXTGLOB)
-
else
-
files = [path]
-
end
-
-
files.each do |filename|
-
begin
-
io_stack << StringIO.new(File.read(filename))
-
rescue
-
raise ConfigError, "could not include file '%s'" % filename
-
end
-
end
-
when /\A([^:\s]*)(?:::([^:\s]*))?\s*=(.*)\z/
-
if $2
-
section = $1
-
key = $2
-
else
-
key = $1
-
end
-
value = unescape_value(data, section, $3)
-
(data[section] ||= {})[key] = value.strip
-
else
-
raise ConfigError, "missing equal sign"
-
end
-
end
-
data
-
end
-
-
# escape with backslash
-
1
QUOTE_REGEXP_SQ = /\A([^'\\]*(?:\\.[^'\\]*)*)'/
-
# escape with backslash and doubled dq
-
1
QUOTE_REGEXP_DQ = /\A([^"\\]*(?:""[^"\\]*|\\.[^"\\]*)*)"/
-
# escaped char map
-
ESCAPE_MAP = {
-
1
"r" => "\r",
-
"n" => "\n",
-
"b" => "\b",
-
"t" => "\t",
-
}
-
-
1
def unescape_value(data, section, value)
-
scanned = []
-
while m = value.match(/['"\\$]/)
-
scanned << m.pre_match
-
c = m[0]
-
value = m.post_match
-
case c
-
when "'"
-
if m = value.match(QUOTE_REGEXP_SQ)
-
scanned << m[1].gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when '"'
-
if m = value.match(QUOTE_REGEXP_DQ)
-
scanned << m[1].gsub(/""/, '').gsub(/\\(.)/, '\\1')
-
value = m.post_match
-
else
-
break
-
end
-
when "\\"
-
c = value.slice!(0, 1)
-
scanned << (ESCAPE_MAP[c] || c)
-
when "$"
-
ref, value = extract_reference(value)
-
refsec = section
-
if ref.index('::')
-
refsec, ref = ref.split('::', 2)
-
end
-
if v = get_key_string(data, refsec, ref)
-
scanned << v
-
else
-
raise ConfigError, "variable has no value"
-
end
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << value
-
scanned.join
-
end
-
-
1
def extract_reference(value)
-
rest = ''
-
if m = value.match(/\(([^)]*)\)|\{([^}]*)\}/)
-
value = m[1] || m[2]
-
rest = m.post_match
-
elsif [?(, ?{].include?(value[0])
-
raise ConfigError, "no close brace"
-
end
-
if m = value.match(/[a-zA-Z0-9_]*(?:::[a-zA-Z0-9_]*)?/)
-
return m[0], m.post_match + rest
-
else
-
raise
-
end
-
end
-
-
1
def clear_comments(line)
-
# FCOMMENT
-
if m = line.match(/\A([\t\n\f ]*);.*\z/)
-
return m[1]
-
end
-
# COMMENT
-
scanned = []
-
while m = line.match(/[#'"\\]/)
-
scanned << m.pre_match
-
c = m[0]
-
line = m.post_match
-
case c
-
when '#'
-
line = nil
-
break
-
when "'", '"'
-
regexp = (c == "'") ? QUOTE_REGEXP_SQ : QUOTE_REGEXP_DQ
-
scanned << c
-
if m = line.match(regexp)
-
scanned << m[0]
-
line = m.post_match
-
else
-
scanned << line
-
line = nil
-
break
-
end
-
when "\\"
-
scanned << c
-
scanned << line.slice!(0, 1)
-
else
-
raise 'must not reaced'
-
end
-
end
-
scanned << line
-
scanned.join
-
end
-
-
1
def get_definition(io_stack)
-
if line = get_line(io_stack)
-
while /[^\\]\\\z/ =~ line
-
if extra = get_line(io_stack)
-
line += extra
-
else
-
break
-
end
-
end
-
return line.strip
-
end
-
end
-
-
1
def get_line(io_stack)
-
while io = io_stack.last
-
if line = io.gets
-
return line.gsub(/[\r\n]*/, '')
-
end
-
io_stack.pop
-
end
-
end
-
end
-
-
##
-
# Creates an instance of OpenSSL's configuration class.
-
#
-
# This can be used in contexts like OpenSSL::X509::ExtensionFactory.config=
-
#
-
# If the optional _filename_ parameter is provided, then it is read in and
-
# parsed via #parse_config.
-
#
-
# This can raise IO exceptions based on the access, or availability of the
-
# file. A ConfigError exception may be raised depending on the validity of
-
# the data being configured.
-
#
-
1
def initialize(filename = nil)
-
@data = {}
-
if filename
-
File.open(filename.to_s) do |file|
-
Config.parse_config(file).each do |section, hash|
-
self[section] = hash
-
end
-
end
-
end
-
end
-
-
##
-
# Gets the value of _key_ from the given _section_
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a specific value from the config if you know the _section_
-
# and _key_ like so:
-
#
-
# config.get_value('default','foo')
-
# #=> "bar"
-
#
-
1
def get_value(section, key)
-
if section.nil?
-
raise TypeError.new('nil not allowed')
-
end
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
#
-
# *Deprecated*
-
#
-
# Use #get_value instead
-
1
def value(arg1, arg2 = nil) # :nodoc:
-
warn('Config#value is deprecated; use Config#get_value')
-
if arg2.nil?
-
section, key = 'default', arg1
-
else
-
section, key = arg1, arg2
-
end
-
section ||= 'default'
-
section = 'default' if section.empty?
-
get_key_string(section, key)
-
end
-
-
##
-
# Set the target _key_ with a given _value_ under a specific _section_.
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can set the value of _foo_ under the _default_ section to a new
-
# value:
-
#
-
# config.add_value('default', 'foo', 'buzz')
-
# #=> "buzz"
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=buzz
-
#
-
1
def add_value(section, key, value)
-
check_modify
-
(@data[section] ||= {})[key] = value
-
end
-
-
##
-
# Get a specific _section_ from the current configuration
-
#
-
# Given the following configurating file being loaded:
-
#
-
# config = OpenSSL::Config.load('foo.cnf')
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
#
-
# You can get a hash of the specific section like so:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar"}
-
#
-
1
def [](section)
-
@data[section] || {}
-
end
-
-
##
-
# Deprecated
-
#
-
# Use #[] instead
-
1
def section(name) # :nodoc:
-
warn('Config#section is deprecated; use Config#[]')
-
@data[name] || {}
-
end
-
-
##
-
# Sets a specific _section_ name with a Hash _pairs_.
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# It's important to note that this will essentially merge any of the keys
-
# in _pairs_ with the existing _section_. For example:
-
#
-
# config['default']
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# config['default'] = {"foo" => "changed"}
-
# #=> {"foo"=>"changed"}
-
# config['default']
-
# #=> {"foo"=>"changed", "baz"=>"buz"}
-
#
-
1
def []=(section, pairs)
-
check_modify
-
@data[section] ||= {}
-
pairs.each do |key, value|
-
self.add_value(section, key, value)
-
end
-
end
-
-
##
-
# Get the names of all sections in the current configuration
-
1
def sections
-
@data.keys
-
end
-
-
##
-
# Get the parsable form of the current configuration
-
#
-
# Given the following configuration being created:
-
#
-
# config = OpenSSL::Config.new
-
# #=> #<OpenSSL::Config sections=[]>
-
# config['default'] = {"foo"=>"bar","baz"=>"buz"}
-
# #=> {"foo"=>"bar", "baz"=>"buz"}
-
# puts config.to_s
-
# #=> [ default ]
-
# # foo=bar
-
# # baz=buz
-
#
-
# You can parse get the serialized configuration using #to_s and then parse
-
# it later:
-
#
-
# serialized_config = config.to_s
-
# # much later...
-
# new_config = OpenSSL::Config.parse(serialized_config)
-
# #=> #<OpenSSL::Config sections=["default"]>
-
# puts new_config
-
# #=> [ default ]
-
# foo=bar
-
# baz=buz
-
#
-
1
def to_s
-
ary = []
-
@data.keys.sort.each do |section|
-
ary << "[ #{section} ]\n"
-
@data[section].keys.each do |key|
-
ary << "#{key}=#{@data[section][key]}\n"
-
end
-
ary << "\n"
-
end
-
ary.join
-
end
-
-
##
-
# For a block.
-
#
-
# Receive the section and its pairs for the current configuration.
-
#
-
# config.each do |section, key, value|
-
# # ...
-
# end
-
#
-
1
def each
-
@data.each do |section, hash|
-
hash.each do |key, value|
-
yield [section, key, value]
-
end
-
end
-
end
-
-
##
-
# String representation of this configuration object, including the class
-
# name and its sections.
-
1
def inspect
-
"#<#{self.class.name} sections=#{sections.inspect}>"
-
end
-
-
1
protected
-
-
1
def data # :nodoc:
-
@data
-
end
-
-
1
private
-
-
1
def initialize_copy(other)
-
@data = other.data.dup
-
end
-
-
1
def check_modify
-
raise TypeError.new("Insecure: can't modify OpenSSL config") if frozen?
-
end
-
-
1
def get_key_string(section, key)
-
Config.get_key_string(@data, section, key)
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space predefined Digest subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
class Digest
-
-
1
alg = %w(MD2 MD4 MD5 MDC2 RIPEMD160 SHA1 SHA224 SHA256 SHA384 SHA512)
-
1
if OPENSSL_VERSION_NUMBER < 0x10100000
-
alg += %w(DSS DSS1 SHA)
-
end
-
-
# Return the hash value computed with _name_ Digest. _name_ is either the
-
# long name or short name of a supported digest algorithm.
-
#
-
# === Examples
-
#
-
# OpenSSL::Digest.digest("SHA256", "abc")
-
#
-
# which is equivalent to:
-
#
-
# OpenSSL::Digest::SHA256.digest("abc")
-
-
1
def self.digest(name, data)
-
super(data, name)
-
end
-
-
1
alg.each{|name|
-
10
klass = Class.new(self) {
-
11
define_method(:initialize, ->(data = nil) {super(name, data)})
-
}
-
20
singleton = (class << klass; self; end)
-
10
singleton.class_eval{
-
10
define_method(:digest){|data| new.digest(data) }
-
10
define_method(:hexdigest){|data| new.hexdigest(data) }
-
}
-
10
const_set(name, klass)
-
}
-
-
# Deprecated.
-
#
-
# This class is only provided for backwards compatibility.
-
# Use OpenSSL::Digest instead.
-
1
class Digest < Digest; end # :nodoc:
-
1
deprecate_constant :Digest
-
-
end # Digest
-
-
# Returns a Digest subclass by _name_
-
#
-
# require 'openssl'
-
#
-
# OpenSSL::Digest("MD5")
-
# # => OpenSSL::Digest::MD5
-
#
-
# Digest("Foo")
-
# # => NameError: wrong constant name Foo
-
-
1
def Digest(name)
-
OpenSSL::Digest.const_get(name)
-
end
-
-
1
module_function :Digest
-
-
end # OpenSSL
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL
-
1
module PKCS5
-
1
module_function
-
-
# OpenSSL::PKCS5.pbkdf2_hmac has been renamed to OpenSSL::KDF.pbkdf2_hmac.
-
# This method is provided for backwards compatibility.
-
1
def pbkdf2_hmac(pass, salt, iter, keylen, digest)
-
OpenSSL::KDF.pbkdf2_hmac(pass, salt: salt, iterations: iter,
-
length: keylen, hash: digest)
-
end
-
-
1
def pbkdf2_hmac_sha1(pass, salt, iter, keylen)
-
pbkdf2_hmac(pass, salt, iter, keylen, "sha1")
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# Ruby/OpenSSL Project
-
# Copyright (C) 2017 Ruby/OpenSSL Project Authors
-
#++
-
-
1
module OpenSSL::PKey
-
1
if defined?(EC)
-
1
class EC::Point
-
# :call-seq:
-
# point.to_bn([conversion_form]) -> OpenSSL::BN
-
#
-
# Returns the octet string representation of the EC point as an instance of
-
# OpenSSL::BN.
-
#
-
# If _conversion_form_ is not given, the _point_conversion_form_ attribute
-
# set to the group is used.
-
#
-
# See #to_octet_string for more information.
-
1
def to_bn(conversion_form = group.point_conversion_form)
-
OpenSSL::BN.new(to_octet_string(conversion_form), 2)
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
=begin
-
= Info
-
'OpenSSL for Ruby 2' project
-
Copyright (C) 2001 GOTOU YUUZOU <gotoyuzo@notwork.org>
-
All rights reserved.
-
-
= Licence
-
This program is licensed under the same licence as Ruby.
-
(See the file 'LICENCE'.)
-
=end
-
-
1
require "openssl/buffering"
-
1
require "io/nonblock"
-
1
require "ipaddr"
-
-
1
module OpenSSL
-
1
module SSL
-
1
class SSLContext
-
DEFAULT_PARAMS = { # :nodoc:
-
1
:min_version => OpenSSL::SSL::TLS1_VERSION,
-
:verify_mode => OpenSSL::SSL::VERIFY_PEER,
-
:verify_hostname => true,
-
:options => -> {
-
1
opts = OpenSSL::SSL::OP_ALL
-
1
opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS
-
1
opts |= OpenSSL::SSL::OP_NO_COMPRESSION
-
1
opts
-
}.call
-
}
-
-
1
if defined?(OpenSSL::PKey::DH)
-
1
DEFAULT_2048 = OpenSSL::PKey::DH.new <<-_end_of_pem_
-
-----BEGIN DH PARAMETERS-----
-
MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY
-
JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab
-
VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6
-
YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3
-
1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD
-
7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg==
-
-----END DH PARAMETERS-----
-
_end_of_pem_
-
1
private_constant :DEFAULT_2048
-
-
1
DEFAULT_TMP_DH_CALLBACK = lambda { |ctx, is_export, keylen| # :nodoc:
-
warn "using default DH parameters." if $VERBOSE
-
DEFAULT_2048
-
}
-
end
-
-
1
if !(OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") &&
-
OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10100000)
-
DEFAULT_PARAMS.merge!(
-
ciphers: %w{
-
ECDHE-ECDSA-AES128-GCM-SHA256
-
ECDHE-RSA-AES128-GCM-SHA256
-
ECDHE-ECDSA-AES256-GCM-SHA384
-
ECDHE-RSA-AES256-GCM-SHA384
-
DHE-RSA-AES128-GCM-SHA256
-
DHE-DSS-AES128-GCM-SHA256
-
DHE-RSA-AES256-GCM-SHA384
-
DHE-DSS-AES256-GCM-SHA384
-
ECDHE-ECDSA-AES128-SHA256
-
ECDHE-RSA-AES128-SHA256
-
ECDHE-ECDSA-AES128-SHA
-
ECDHE-RSA-AES128-SHA
-
ECDHE-ECDSA-AES256-SHA384
-
ECDHE-RSA-AES256-SHA384
-
ECDHE-ECDSA-AES256-SHA
-
ECDHE-RSA-AES256-SHA
-
DHE-RSA-AES128-SHA256
-
DHE-RSA-AES256-SHA256
-
DHE-RSA-AES128-SHA
-
DHE-RSA-AES256-SHA
-
DHE-DSS-AES128-SHA256
-
DHE-DSS-AES256-SHA256
-
DHE-DSS-AES128-SHA
-
DHE-DSS-AES256-SHA
-
AES128-GCM-SHA256
-
AES256-GCM-SHA384
-
AES128-SHA256
-
AES256-SHA256
-
AES128-SHA
-
AES256-SHA
-
}.join(":"),
-
)
-
end
-
-
1
DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc:
-
1
DEFAULT_CERT_STORE.set_default_paths
-
1
DEFAULT_CERT_STORE.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
-
-
# A callback invoked when DH parameters are required.
-
#
-
# The callback is invoked with the Session for the key exchange, an
-
# flag indicating the use of an export cipher and the keylength
-
# required.
-
#
-
# The callback must return an OpenSSL::PKey::DH instance of the correct
-
# key length.
-
-
1
attr_accessor :tmp_dh_callback
-
-
# A callback invoked at connect time to distinguish between multiple
-
# server names.
-
#
-
# The callback is invoked with an SSLSocket and a server name. The
-
# callback must return an SSLContext for the server name or nil.
-
1
attr_accessor :servername_cb
-
-
# call-seq:
-
# SSLContext.new -> ctx
-
# SSLContext.new(:TLSv1) -> ctx
-
# SSLContext.new("SSLv23") -> ctx
-
#
-
# Creates a new SSL context.
-
#
-
# If an argument is given, #ssl_version= is called with the value. Note
-
# that this form is deprecated. New applications should use #min_version=
-
# and #max_version= as necessary.
-
1
def initialize(version = nil)
-
self.options |= OpenSSL::SSL::OP_ALL
-
self.ssl_version = version if version
-
end
-
-
##
-
# call-seq:
-
# ctx.set_params(params = {}) -> params
-
#
-
# Sets saner defaults optimized for the use with HTTP-like protocols.
-
#
-
# If a Hash _params_ is given, the parameters are overridden with it.
-
# The keys in _params_ must be assignment methods on SSLContext.
-
#
-
# If the verify_mode is not VERIFY_NONE and ca_file, ca_path and
-
# cert_store are not set then the system default certificate store is
-
# used.
-
1
def set_params(params={})
-
params = DEFAULT_PARAMS.merge(params)
-
self.options = params.delete(:options) # set before min_version/max_version
-
params.each{|name, value| self.__send__("#{name}=", value) }
-
if self.verify_mode != OpenSSL::SSL::VERIFY_NONE
-
unless self.ca_file or self.ca_path or self.cert_store
-
self.cert_store = DEFAULT_CERT_STORE
-
end
-
end
-
return params
-
end
-
-
# call-seq:
-
# ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.min_version = :TLS1_2
-
# ctx.min_version = nil
-
#
-
# Sets the lower bound on the supported SSL/TLS protocol version. The
-
# version may be specified by an integer constant named
-
# OpenSSL::SSL::*_VERSION, a Symbol, or +nil+ which means "any version".
-
#
-
# Be careful that you don't overwrite OpenSSL::SSL::OP_NO_{SSL,TLS}v*
-
# options by #options= once you have called #min_version= or
-
# #max_version=.
-
#
-
# === Example
-
# ctx = OpenSSL::SSL::SSLContext.new
-
# ctx.min_version = OpenSSL::SSL::TLS1_1_VERSION
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
#
-
# sock = OpenSSL::SSL::SSLSocket.new(tcp_sock, ctx)
-
# sock.connect # Initiates a connection using either TLS 1.1 or TLS 1.2
-
1
def min_version=(version)
-
set_minmax_proto_version(version, @max_proto_version ||= nil)
-
@min_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.max_version = OpenSSL::SSL::TLS1_2_VERSION
-
# ctx.max_version = :TLS1_2
-
# ctx.max_version = nil
-
#
-
# Sets the upper bound of the supported SSL/TLS protocol version. See
-
# #min_version= for the possible values.
-
1
def max_version=(version)
-
set_minmax_proto_version(@min_proto_version ||= nil, version)
-
@max_proto_version = version
-
end
-
-
# call-seq:
-
# ctx.ssl_version = :TLSv1
-
# ctx.ssl_version = "SSLv23"
-
#
-
# Sets the SSL/TLS protocol version for the context. This forces
-
# connections to use only the specified protocol version. This is
-
# deprecated and only provided for backwards compatibility. Use
-
# #min_version= and #max_version= instead.
-
#
-
# === History
-
# As the name hints, this used to call the SSL_CTX_set_ssl_version()
-
# function which sets the SSL method used for connections created from
-
# the context. As of Ruby/OpenSSL 2.1, this accessor method is
-
# implemented to call #min_version= and #max_version= instead.
-
1
def ssl_version=(meth)
-
meth = meth.to_s if meth.is_a?(Symbol)
-
if /(?<type>_client|_server)\z/ =~ meth
-
meth = $`
-
if $VERBOSE
-
warn "#{caller(1, 1)[0]}: method type #{type.inspect} is ignored"
-
end
-
end
-
version = METHODS_MAP[meth.intern] or
-
raise ArgumentError, "unknown SSL method `%s'" % meth
-
set_minmax_proto_version(version, version)
-
@min_proto_version = @max_proto_version = version
-
end
-
-
METHODS_MAP = {
-
1
SSLv23: 0,
-
SSLv2: OpenSSL::SSL::SSL2_VERSION,
-
SSLv3: OpenSSL::SSL::SSL3_VERSION,
-
TLSv1: OpenSSL::SSL::TLS1_VERSION,
-
TLSv1_1: OpenSSL::SSL::TLS1_1_VERSION,
-
TLSv1_2: OpenSSL::SSL::TLS1_2_VERSION,
-
}.freeze
-
1
private_constant :METHODS_MAP
-
-
# The list of available SSL/TLS methods. This constant is only provided
-
# for backwards compatibility.
-
1
METHODS = METHODS_MAP.flat_map { |name,|
-
6
[name, :"#{name}_client", :"#{name}_server"]
-
}.freeze
-
1
deprecate_constant :METHODS
-
end
-
-
1
module SocketForwarder
-
1
def addr
-
to_io.addr
-
end
-
-
1
def peeraddr
-
to_io.peeraddr
-
end
-
-
1
def setsockopt(level, optname, optval)
-
to_io.setsockopt(level, optname, optval)
-
end
-
-
1
def getsockopt(level, optname)
-
to_io.getsockopt(level, optname)
-
end
-
-
1
def fcntl(*args)
-
to_io.fcntl(*args)
-
end
-
-
1
def closed?
-
to_io.closed?
-
end
-
-
1
def do_not_reverse_lookup=(flag)
-
to_io.do_not_reverse_lookup = flag
-
end
-
end
-
-
1
def verify_certificate_identity(cert, hostname)
-
should_verify_common_name = true
-
cert.extensions.each{|ext|
-
next if ext.oid != "subjectAltName"
-
ostr = OpenSSL::ASN1.decode(ext.to_der).value.last
-
sequence = OpenSSL::ASN1.decode(ostr.value)
-
sequence.value.each{|san|
-
case san.tag
-
when 2 # dNSName in GeneralName (RFC5280)
-
should_verify_common_name = false
-
return true if verify_hostname(hostname, san.value)
-
when 7 # iPAddress in GeneralName (RFC5280)
-
should_verify_common_name = false
-
if san.value.size == 4 || san.value.size == 16
-
begin
-
return true if san.value == IPAddr.new(hostname).hton
-
rescue IPAddr::InvalidAddressError
-
end
-
end
-
end
-
}
-
}
-
if should_verify_common_name
-
cert.subject.to_a.each{|oid, value|
-
if oid == "CN"
-
return true if verify_hostname(hostname, value)
-
end
-
}
-
end
-
return false
-
end
-
1
module_function :verify_certificate_identity
-
-
1
def verify_hostname(hostname, san) # :nodoc:
-
# RFC 5280, IA5String is limited to the set of ASCII characters
-
return false unless san.ascii_only?
-
return false unless hostname.ascii_only?
-
-
# See RFC 6125, section 6.4.1
-
# Matching is case-insensitive.
-
san_parts = san.downcase.split(".")
-
-
# TODO: this behavior should probably be more strict
-
return san == hostname if san_parts.size < 2
-
-
# Matching is case-insensitive.
-
host_parts = hostname.downcase.split(".")
-
-
# RFC 6125, section 6.4.3, subitem 2.
-
# If the wildcard character is the only character of the left-most
-
# label in the presented identifier, the client SHOULD NOT compare
-
# against anything but the left-most label of the reference
-
# identifier (e.g., *.example.com would match foo.example.com but
-
# not bar.foo.example.com or example.com).
-
return false unless san_parts.size == host_parts.size
-
-
# RFC 6125, section 6.4.3, subitem 1.
-
# The client SHOULD NOT attempt to match a presented identifier in
-
# which the wildcard character comprises a label other than the
-
# left-most label (e.g., do not match bar.*.example.net).
-
return false unless verify_wildcard(host_parts.shift, san_parts.shift)
-
-
san_parts.join(".") == host_parts.join(".")
-
end
-
1
module_function :verify_hostname
-
-
1
def verify_wildcard(domain_component, san_component) # :nodoc:
-
parts = san_component.split("*", -1)
-
-
return false if parts.size > 2
-
return san_component == domain_component if parts.size == 1
-
-
# RFC 6125, section 6.4.3, subitem 3.
-
# The client SHOULD NOT attempt to match a presented identifier
-
# where the wildcard character is embedded within an A-label or
-
# U-label of an internationalized domain name.
-
return false if domain_component.start_with?("xn--") && san_component != "*"
-
-
parts[0].length + parts[1].length < domain_component.length &&
-
domain_component.start_with?(parts[0]) &&
-
domain_component.end_with?(parts[1])
-
end
-
1
module_function :verify_wildcard
-
-
1
class SSLSocket
-
1
include Buffering
-
1
include SocketForwarder
-
-
1
attr_reader :hostname
-
-
# The underlying IO object.
-
1
attr_reader :io
-
1
alias :to_io :io
-
-
# The SSLContext object used in this connection.
-
1
attr_reader :context
-
-
# Whether to close the underlying socket as well, when the SSL/TLS
-
# connection is shut down. This defaults to +false+.
-
1
attr_accessor :sync_close
-
-
# call-seq:
-
# ssl.sysclose => nil
-
#
-
# Sends "close notify" to the peer and tries to shut down the SSL
-
# connection gracefully.
-
#
-
# If sync_close is set to +true+, the underlying IO is also closed.
-
1
def sysclose
-
return if closed?
-
stop
-
io.close if sync_close
-
end
-
-
# call-seq:
-
# ssl.post_connection_check(hostname) -> true
-
#
-
# Perform hostname verification following RFC 6125.
-
#
-
# This method MUST be called after calling #connect to ensure that the
-
# hostname of a remote peer has been verified.
-
1
def post_connection_check(hostname)
-
if peer_cert.nil?
-
msg = "Peer verification enabled, but no certificate received."
-
if using_anon_cipher?
-
msg += " Anonymous cipher suite #{cipher[0]} was negotiated. " \
-
"Anonymous suites must be disabled to use peer verification."
-
end
-
raise SSLError, msg
-
end
-
-
unless OpenSSL::SSL.verify_certificate_identity(peer_cert, hostname)
-
raise SSLError, "hostname \"#{hostname}\" does not match the server certificate"
-
end
-
return true
-
end
-
-
# call-seq:
-
# ssl.session -> aSession
-
#
-
# Returns the SSLSession object currently used, or nil if the session is
-
# not established.
-
1
def session
-
SSL::Session.new(self)
-
rescue SSL::Session::SessionError
-
nil
-
end
-
-
1
private
-
-
1
def using_anon_cipher?
-
ctx = OpenSSL::SSL::SSLContext.new
-
ctx.ciphers = "aNULL"
-
ctx.ciphers.include?(cipher)
-
end
-
-
1
def client_cert_cb
-
@context.client_cert_cb
-
end
-
-
1
def tmp_dh_callback
-
@context.tmp_dh_callback || OpenSSL::SSL::SSLContext::DEFAULT_TMP_DH_CALLBACK
-
end
-
-
1
def tmp_ecdh_callback
-
@context.tmp_ecdh_callback
-
end
-
-
1
def session_new_cb
-
@context.session_new_cb
-
end
-
-
1
def session_get_cb
-
@context.session_get_cb
-
end
-
end
-
-
##
-
# SSLServer represents a TCP/IP server socket with Secure Sockets Layer.
-
1
class SSLServer
-
1
include SocketForwarder
-
# When true then #accept works exactly the same as TCPServer#accept
-
1
attr_accessor :start_immediately
-
-
# Creates a new instance of SSLServer.
-
# * _srv_ is an instance of TCPServer.
-
# * _ctx_ is an instance of OpenSSL::SSL::SSLContext.
-
1
def initialize(svr, ctx)
-
@svr = svr
-
@ctx = ctx
-
unless ctx.session_id_context
-
# see #6137 - session id may not exceed 32 bytes
-
prng = ::Random.new($0.hash)
-
session_id = prng.bytes(16).unpack('H*')[0]
-
@ctx.session_id_context = session_id
-
end
-
@start_immediately = true
-
end
-
-
# Returns the TCPServer passed to the SSLServer when initialized.
-
1
def to_io
-
@svr
-
end
-
-
# See TCPServer#listen for details.
-
1
def listen(backlog=5)
-
@svr.listen(backlog)
-
end
-
-
# See BasicSocket#shutdown for details.
-
1
def shutdown(how=Socket::SHUT_RDWR)
-
@svr.shutdown(how)
-
end
-
-
# Works similar to TCPServer#accept.
-
1
def accept
-
# Socket#accept returns [socket, addrinfo].
-
# TCPServer#accept returns a socket.
-
# The following comma strips addrinfo.
-
sock, = @svr.accept
-
begin
-
ssl = OpenSSL::SSL::SSLSocket.new(sock, @ctx)
-
ssl.sync_close = true
-
ssl.accept if @start_immediately
-
ssl
-
rescue Exception => ex
-
if ssl
-
ssl.close
-
else
-
sock.close
-
end
-
raise ex
-
end
-
end
-
-
# See IO#close for details.
-
1
def close
-
@svr.close
-
end
-
end
-
end
-
end
-
# frozen_string_literal: false
-
#--
-
# = Ruby-space definitions that completes C-space funcs for X509 and subclasses
-
#
-
# = Info
-
# 'OpenSSL for Ruby 2' project
-
# Copyright (C) 2002 Michal Rokos <m.rokos@sh.cvut.cz>
-
# All rights reserved.
-
#
-
# = Licence
-
# This program is licensed under the same licence as Ruby.
-
# (See the file 'LICENCE'.)
-
#++
-
-
1
module OpenSSL
-
1
module X509
-
1
class ExtensionFactory
-
1
def create_extension(*arg)
-
if arg.size > 1
-
create_ext(*arg)
-
else
-
send("create_ext_from_"+arg[0].class.name.downcase, arg[0])
-
end
-
end
-
-
1
def create_ext_from_array(ary)
-
raise ExtensionError, "unexpected array form" if ary.size > 3
-
create_ext(ary[0], ary[1], ary[2])
-
end
-
-
1
def create_ext_from_string(str) # "oid = critical, value"
-
oid, value = str.split(/=/, 2)
-
oid.strip!
-
value.strip!
-
create_ext(oid, value)
-
end
-
-
1
def create_ext_from_hash(hash)
-
create_ext(hash["oid"], hash["value"], hash["critical"])
-
end
-
end
-
-
1
class Extension
-
1
def ==(other)
-
return false unless Extension === other
-
to_der == other.to_der
-
end
-
-
1
def to_s # "oid = critical, value"
-
str = self.oid
-
str << " = "
-
str << "critical, " if self.critical?
-
str << self.value.gsub(/\n/, ", ")
-
end
-
-
1
def to_h # {"oid"=>sn|ln, "value"=>value, "critical"=>true|false}
-
{"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?}
-
end
-
-
1
def to_a
-
[ self.oid, self.value, self.critical? ]
-
end
-
end
-
-
1
class Name
-
1
module RFC2253DN
-
1
Special = ',=+<>#;'
-
1
HexChar = /[0-9a-fA-F]/
-
1
HexPair = /#{HexChar}#{HexChar}/
-
1
HexString = /#{HexPair}+/
-
1
Pair = /\\(?:[#{Special}]|\\|"|#{HexPair})/
-
1
StringChar = /[^\\"#{Special}]/
-
1
QuoteChar = /[^\\"]/
-
1
AttributeType = /[a-zA-Z][0-9a-zA-Z]*|[0-9]+(?:\.[0-9]+)*/
-
1
AttributeValue = /
-
(?!["#])((?:#{StringChar}|#{Pair})*)|
-
\#(#{HexString})|
-
"((?:#{QuoteChar}|#{Pair})*)"
-
/x
-
1
TypeAndValue = /\A(#{AttributeType})=#{AttributeValue}/
-
-
1
module_function
-
-
1
def expand_pair(str)
-
return nil unless str
-
return str.gsub(Pair){
-
pair = $&
-
case pair.size
-
when 2 then pair[1,1]
-
when 3 then Integer("0x#{pair[1,2]}").chr
-
else raise OpenSSL::X509::NameError, "invalid pair: #{str}"
-
end
-
}
-
end
-
-
1
def expand_hexstring(str)
-
return nil unless str
-
der = str.gsub(HexPair){$&.to_i(16).chr }
-
a1 = OpenSSL::ASN1.decode(der)
-
return a1.value, a1.tag
-
end
-
-
1
def expand_value(str1, str2, str3)
-
value = expand_pair(str1)
-
value, tag = expand_hexstring(str2) unless value
-
value = expand_pair(str3) unless value
-
return value, tag
-
end
-
-
1
def scan(dn)
-
str = dn
-
ary = []
-
while true
-
if md = TypeAndValue.match(str)
-
remain = md.post_match
-
type = md[1]
-
value, tag = expand_value(md[2], md[3], md[4]) rescue nil
-
if value
-
type_and_value = [type, value]
-
type_and_value.push(tag) if tag
-
ary.unshift(type_and_value)
-
if remain.length > 2 && remain[0] == ?,
-
str = remain[1..-1]
-
next
-
elsif remain.length > 2 && remain[0] == ?+
-
raise OpenSSL::X509::NameError,
-
"multi-valued RDN is not supported: #{dn}"
-
elsif remain.empty?
-
break
-
end
-
end
-
end
-
msg_dn = dn[0, dn.length - str.length] + " =>" + str
-
raise OpenSSL::X509::NameError, "malformed RDN: #{msg_dn}"
-
end
-
return ary
-
end
-
end
-
-
1
class << self
-
1
def parse_rfc2253(str, template=OBJECT_TYPE_TEMPLATE)
-
ary = OpenSSL::X509::Name::RFC2253DN.scan(str)
-
self.new(ary, template)
-
end
-
-
1
def parse_openssl(str, template=OBJECT_TYPE_TEMPLATE)
-
if str.start_with?("/")
-
# /A=B/C=D format
-
ary = str[1..-1].split("/").map { |i| i.split("=", 2) }
-
else
-
# Comma-separated
-
ary = str.split(",").map { |i| i.strip.split("=", 2) }
-
end
-
self.new(ary, template)
-
end
-
-
1
alias parse parse_openssl
-
end
-
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.text ' '
-
q.text to_s(OpenSSL::X509::Name::RFC2253)
-
}
-
end
-
end
-
-
1
class Attribute
-
1
def ==(other)
-
return false unless Attribute === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class StoreContext
-
1
def cleanup
-
warn "(#{caller.first}) OpenSSL::X509::StoreContext#cleanup is deprecated with no replacement" if $VERBOSE
-
end
-
end
-
-
1
class Certificate
-
1
def pretty_print(q)
-
q.object_group(self) {
-
q.breakable
-
q.text 'subject='; q.pp self.subject; q.text ','; q.breakable
-
q.text 'issuer='; q.pp self.issuer; q.text ','; q.breakable
-
q.text 'serial='; q.pp self.serial; q.text ','; q.breakable
-
q.text 'not_before='; q.pp self.not_before; q.text ','; q.breakable
-
q.text 'not_after='; q.pp self.not_after
-
}
-
end
-
end
-
-
1
class CRL
-
1
def ==(other)
-
return false unless CRL === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Revoked
-
1
def ==(other)
-
return false unless Revoked === other
-
to_der == other.to_der
-
end
-
end
-
-
1
class Request
-
1
def ==(other)
-
return false unless Request === other
-
to_der == other.to_der
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
#
-
# optparse.rb - command-line option analysis with the OptionParser class.
-
#
-
# Author:: Nobu Nakada
-
# Documentation:: Nobu Nakada and Gavin Sinclair.
-
#
-
# See OptionParser for documentation.
-
#
-
-
-
#--
-
# == Developer Documentation (not for RDoc output)
-
#
-
# === Class tree
-
#
-
# - OptionParser:: front end
-
# - OptionParser::Switch:: each switches
-
# - OptionParser::List:: options list
-
# - OptionParser::ParseError:: errors on parsing
-
# - OptionParser::AmbiguousOption
-
# - OptionParser::NeedlessArgument
-
# - OptionParser::MissingArgument
-
# - OptionParser::InvalidOption
-
# - OptionParser::InvalidArgument
-
# - OptionParser::AmbiguousArgument
-
#
-
# === Object relationship diagram
-
#
-
# +--------------+
-
# | OptionParser |<>-----+
-
# +--------------+ | +--------+
-
# | ,-| Switch |
-
# on_head -------->+---------------+ / +--------+
-
# accept/reject -->| List |<|>-
-
# | |<|>- +----------+
-
# on ------------->+---------------+ `-| argument |
-
# : : | class |
-
# +---------------+ |==========|
-
# on_tail -------->| | |pattern |
-
# +---------------+ |----------|
-
# OptionParser.accept ->| DefaultList | |converter |
-
# reject |(shared between| +----------+
-
# | all instances)|
-
# +---------------+
-
#
-
#++
-
#
-
# == OptionParser
-
#
-
# === Introduction
-
#
-
# OptionParser is a class for command-line option analysis. It is much more
-
# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented
-
# solution.
-
#
-
# === Features
-
#
-
# 1. The argument specification and the code to handle it are written in the
-
# same place.
-
# 2. It can output an option summary; you don't need to maintain this string
-
# separately.
-
# 3. Optional and mandatory arguments are specified very gracefully.
-
# 4. Arguments can be automatically converted to a specified class.
-
# 5. Arguments can be restricted to a certain set.
-
#
-
# All of these features are demonstrated in the examples below. See
-
# #make_switch for full documentation.
-
#
-
# === Minimal example
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# options[:verbose] = v
-
# end
-
# end.parse!
-
#
-
# p options
-
# p ARGV
-
#
-
# === Generating Help
-
#
-
# OptionParser can be used to automatically generate help for the commands you
-
# write:
-
#
-
# require 'optparse'
-
#
-
# Options = Struct.new(:name)
-
#
-
# class Parser
-
# def self.parse(options)
-
# args = Options.new("world")
-
#
-
# opt_parser = OptionParser.new do |opts|
-
# opts.banner = "Usage: example.rb [options]"
-
#
-
# opts.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
-
# args.name = n
-
# end
-
#
-
# opts.on("-h", "--help", "Prints this help") do
-
# puts opts
-
# exit
-
# end
-
# end
-
#
-
# opt_parser.parse!(options)
-
# return args
-
# end
-
# end
-
# options = Parser.parse %w[--help]
-
#
-
# #=>
-
# # Usage: example.rb [options]
-
# # -n, --name=NAME Name to say hello to
-
# # -h, --help Prints this help
-
#
-
# === Required Arguments
-
#
-
# For options that require an argument, option specification strings may include an
-
# option name in all caps. If an option is used without the required argument,
-
# an exception will be raised.
-
#
-
# require 'optparse'
-
#
-
# options = {}
-
# OptionParser.new do |parser|
-
# parser.on("-r", "--require LIBRARY",
-
# "Require the LIBRARY before executing your script") do |lib|
-
# puts "You required #{lib}!"
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -r
-
# optparse-test.rb:9:in `<main>': missing argument: -r (OptionParser::MissingArgument)
-
# $ ruby optparse-test.rb -r my-library
-
# You required my-library!
-
#
-
# === Type Coercion
-
#
-
# OptionParser supports the ability to coerce command line arguments
-
# into objects for us.
-
#
-
# OptionParser comes with a few ready-to-use kinds of type
-
# coercion. They are:
-
#
-
# - Date -- Anything accepted by +Date.parse+
-
# - DateTime -- Anything accepted by +DateTime.parse+
-
# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+
-
# - URI -- Anything accepted by +URI.parse+
-
# - Shellwords -- Anything accepted by +Shellwords.shellwords+
-
# - String -- Any non-empty string
-
# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
-
# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
-
# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
-
# - DecimalInteger -- Like +Integer+, but no octal format.
-
# - OctalInteger -- Like +Integer+, but no decimal format.
-
# - DecimalNumeric -- Decimal integer or float.
-
# - TrueClass -- Accepts '+, yes, true, -, no, false' and
-
# defaults as +true+
-
# - FalseClass -- Same as +TrueClass+, but defaults to +false+
-
# - Array -- Strings separated by ',' (e.g. 1,2,3)
-
# - Regexp -- Regular expressions. Also includes options.
-
#
-
# We can also add our own coercions, which we will cover soon.
-
#
-
# ==== Using Built-in Conversions
-
#
-
# As an example, the built-in +Time+ conversion is used. The other built-in
-
# conversions behave in the same way.
-
# OptionParser will attempt to parse the argument
-
# as a +Time+. If it succeeds, that time will be passed to the
-
# handler block. Otherwise, an exception will be raised.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# OptionParser.new do |parser|
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# p time
-
# end
-
# end.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -t nonsense
-
# ... invalid argument: -t nonsense (OptionParser::InvalidArgument)
-
# $ ruby optparse-test.rb -t 10-11-12
-
# 2010-11-12 00:00:00 -0500
-
# $ ruby optparse-test.rb -t 9:30
-
# 2014-08-13 09:30:00 -0400
-
#
-
# ==== Creating Custom Conversions
-
#
-
# The +accept+ method on OptionParser may be used to create converters.
-
# It specifies which conversion block to call whenever a class is specified.
-
# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
-
#
-
# require 'optparse'
-
#
-
# User = Struct.new(:id, :name)
-
#
-
# def find_user id
-
# not_found = ->{ raise "No User Found for id #{id}" }
-
# [ User.new(1, "Sam"),
-
# User.new(2, "Gandalf") ].find(not_found) do |u|
-
# u.id == id
-
# end
-
# end
-
#
-
# op = OptionParser.new
-
# op.accept(User) do |user_id|
-
# find_user user_id.to_i
-
# end
-
#
-
# op.on("--user ID", User) do |user|
-
# puts user
-
# end
-
#
-
# op.parse!
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb --user 1
-
# #<struct User id=1, name="Sam">
-
# $ ruby optparse-test.rb --user 2
-
# #<struct User id=2, name="Gandalf">
-
# $ ruby optparse-test.rb --user 3
-
# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError)
-
#
-
# === Store options to a Hash
-
#
-
# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
-
#
-
# require 'optparse'
-
#
-
# params = {}
-
# OptionParser.new do |opts|
-
# opts.on('-a')
-
# opts.on('-b NUM', Integer)
-
# opts.on('-v', '--verbose')
-
# end.parse!(into: params)
-
#
-
# p params
-
#
-
# Used:
-
#
-
# $ ruby optparse-test.rb -a
-
# {:a=>true}
-
# $ ruby optparse-test.rb -a -v
-
# {:a=>true, :verbose=>true}
-
# $ ruby optparse-test.rb -a -b 100
-
# {:a=>true, :b=>100}
-
#
-
# === Complete example
-
#
-
# The following example is a complete Ruby program. You can run it and see the
-
# effect of specifying various options. This is probably the best way to learn
-
# the features of +optparse+.
-
#
-
# require 'optparse'
-
# require 'optparse/time'
-
# require 'ostruct'
-
# require 'pp'
-
#
-
# class OptparseExample
-
# Version = '1.0.0'
-
#
-
# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# class ScriptOptions
-
# attr_accessor :library, :inplace, :encoding, :transfer_type,
-
# :verbose, :extension, :delay, :time, :record_separator,
-
# :list
-
#
-
# def initialize
-
# self.library = []
-
# self.inplace = false
-
# self.encoding = "utf8"
-
# self.transfer_type = :auto
-
# self.verbose = false
-
# end
-
#
-
# def define_options(parser)
-
# parser.banner = "Usage: example.rb [options]"
-
# parser.separator ""
-
# parser.separator "Specific options:"
-
#
-
# # add additional options
-
# perform_inplace_option(parser)
-
# delay_execution_option(parser)
-
# execute_at_time_option(parser)
-
# specify_record_separator_option(parser)
-
# list_example_option(parser)
-
# specify_encoding_option(parser)
-
# optional_option_argument_with_keyword_completion_option(parser)
-
# boolean_verbose_option(parser)
-
#
-
# parser.separator ""
-
# parser.separator "Common options:"
-
# # No argument, shows at tail. This will print an options summary.
-
# # Try it and see!
-
# parser.on_tail("-h", "--help", "Show this message") do
-
# puts parser
-
# exit
-
# end
-
# # Another typical switch to print the version.
-
# parser.on_tail("--version", "Show version") do
-
# puts Version
-
# exit
-
# end
-
# end
-
#
-
# def perform_inplace_option(parser)
-
# # Specifies an optional option argument
-
# parser.on("-i", "--inplace [EXTENSION]",
-
# "Edit ARGV files in place",
-
# "(make backup if EXTENSION supplied)") do |ext|
-
# self.inplace = true
-
# self.extension = ext || ''
-
# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
-
# end
-
# end
-
#
-
# def delay_execution_option(parser)
-
# # Cast 'delay' argument to a Float.
-
# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
-
# self.delay = n
-
# end
-
# end
-
#
-
# def execute_at_time_option(parser)
-
# # Cast 'time' argument to a Time object.
-
# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
-
# self.time = time
-
# end
-
# end
-
#
-
# def specify_record_separator_option(parser)
-
# # Cast to octal integer.
-
# parser.on("-F", "--irs [OCTAL]", OptionParser::OctalInteger,
-
# "Specify record separator (default \\0)") do |rs|
-
# self.record_separator = rs
-
# end
-
# end
-
#
-
# def list_example_option(parser)
-
# # List of arguments.
-
# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
-
# self.list = list
-
# end
-
# end
-
#
-
# def specify_encoding_option(parser)
-
# # Keyword completion. We are specifying a specific set of arguments (CODES
-
# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
-
# # the shortest unambiguous text.
-
# code_list = (CODE_ALIASES.keys + CODES).join(', ')
-
# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
-
# "(#{code_list})") do |encoding|
-
# self.encoding = encoding
-
# end
-
# end
-
#
-
# def optional_option_argument_with_keyword_completion_option(parser)
-
# # Optional '--type' option argument with keyword completion.
-
# parser.on("--type [TYPE]", [:text, :binary, :auto],
-
# "Select transfer type (text, binary, auto)") do |t|
-
# self.transfer_type = t
-
# end
-
# end
-
#
-
# def boolean_verbose_option(parser)
-
# # Boolean switch.
-
# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
-
# self.verbose = v
-
# end
-
# end
-
# end
-
#
-
# #
-
# # Return a structure describing the options.
-
# #
-
# def parse(args)
-
# # The options specified on the command line will be collected in
-
# # *options*.
-
#
-
# @options = ScriptOptions.new
-
# @args = OptionParser.new do |parser|
-
# @options.define_options(parser)
-
# parser.parse!(args)
-
# end
-
# @options
-
# end
-
#
-
# attr_reader :parser, :options
-
# end # class OptparseExample
-
#
-
# example = OptparseExample.new
-
# options = example.parse(ARGV)
-
# pp options # example.options
-
# pp ARGV
-
#
-
# === Shell Completion
-
#
-
# For modern shells (e.g. bash, zsh, etc.), you can use shell
-
# completion for command line options.
-
#
-
# === Further documentation
-
#
-
# The above examples should be enough to learn how to use this class. If you
-
# have any questions, file a ticket at http://bugs.ruby-lang.org.
-
#
-
1
class OptionParser
-
# :stopdoc:
-
1
NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
-
1
RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
-
1
OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
-
# :startdoc:
-
-
#
-
# Keyword completion module. This allows partial arguments to be specified
-
# and resolved against a list of acceptable values.
-
#
-
1
module Completion
-
1
def self.regexp(key, icase)
-
Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
-
end
-
-
1
def self.candidate(key, icase = false, pat = nil, &block)
-
pat ||= Completion.regexp(key, icase)
-
candidates = []
-
block.call do |k, *v|
-
(if Regexp === k
-
kn = ""
-
k === key
-
else
-
kn = defined?(k.id2name) ? k.id2name : k
-
pat === kn
-
end) or next
-
v << k if v.empty?
-
candidates << [k, v, kn]
-
end
-
candidates
-
end
-
-
1
def candidate(key, icase = false, pat = nil)
-
Completion.candidate(key, icase, pat, &method(:each))
-
end
-
-
1
public
-
1
def complete(key, icase = false, pat = nil)
-
candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
-
if candidates.size == 1
-
canon, sw, * = candidates[0]
-
elsif candidates.size > 1
-
canon, sw, cn = candidates.shift
-
candidates.each do |k, v, kn|
-
next if sw == v
-
if String === cn and String === kn
-
if cn.rindex(kn, 0)
-
canon, sw, cn = k, v, kn
-
next
-
elsif kn.rindex(cn, 0)
-
next
-
end
-
end
-
throw :ambiguous, key
-
end
-
end
-
if canon
-
block_given? or return key, *sw
-
yield(key, *sw)
-
end
-
end
-
-
1
def convert(opt = nil, val = nil, *)
-
val
-
end
-
end
-
-
-
#
-
# Map from option/keyword string to object with completion.
-
#
-
1
class OptionMap < Hash
-
1
include Completion
-
end
-
-
-
#
-
# Individual switch class. Not important to the user.
-
#
-
# Defined within Switch are several Switch-derived classes: NoArgument,
-
# RequiredArgument, etc.
-
#
-
1
class Switch
-
1
attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
-
-
#
-
# Guesses argument style from +arg+. Returns corresponding
-
# OptionParser::Switch class (OptionalArgument, etc.).
-
#
-
1
def self.guess(arg)
-
3
case arg
-
when ""
-
t = self
-
when /\A=?\[/
-
t = Switch::OptionalArgument
-
when /\A\s+\[/
-
t = Switch::PlacedArgument
-
else
-
3
t = Switch::RequiredArgument
-
end
-
3
self >= t or incompatible_argument_styles(arg, t)
-
3
t
-
end
-
-
1
def self.incompatible_argument_styles(arg, t)
-
raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
-
1
def self.pattern
-
NilClass
-
end
-
-
1
def initialize(pattern = nil, conv = nil,
-
short = nil, long = nil, arg = nil,
-
6
desc = ([] if short or long), block = nil, &_block)
-
13
raise if Array === pattern
-
13
block ||= _block
-
@pattern, @conv, @short, @long, @arg, @desc, @block =
-
13
pattern, conv, short, long, arg, desc, block
-
end
-
-
#
-
# Parses +arg+ and returns rest of +arg+ and matched portion to the
-
# argument pattern. Yields when the pattern doesn't match substring.
-
#
-
1
def parse_arg(arg)
-
pattern or return nil, [arg]
-
unless m = pattern.match(arg)
-
yield(InvalidArgument, arg)
-
return arg, []
-
end
-
if String === m
-
m = [s = m]
-
else
-
m = m.to_a
-
s = m[0]
-
return nil, m unless String === s
-
end
-
raise InvalidArgument, arg unless arg.rindex(s, 0)
-
return nil, m if s.length == arg.length
-
yield(InvalidArgument, arg) # didn't match whole arg
-
return arg[s.length..-1], m
-
end
-
1
private :parse_arg
-
-
#
-
# Parses argument, converts and returns +arg+, +block+ and result of
-
# conversion. Yields at semi-error condition instead of raising an
-
# exception.
-
#
-
1
def conv_arg(arg, val = [])
-
if conv
-
val = conv.call(*val)
-
else
-
val = proc {|v| v}.call(*val)
-
end
-
return arg, block, val
-
end
-
1
private :conv_arg
-
-
#
-
# Produces the summary text. Each line of the summary is yielded to the
-
# block (without newline).
-
#
-
# +sdone+:: Already summarized short style options keyed hash.
-
# +ldone+:: Already summarized long style options keyed hash.
-
# +width+:: Width of left side (option part). In other words, the right
-
# side (description part) starts after +width+ columns.
-
# +max+:: Maximum width of left side -> the options are filled within
-
# +max+ columns.
-
# +indent+:: Prefix string indents all summarized lines.
-
#
-
1
def summarize(sdone = [], ldone = [], width = 1, max = width - 1, indent = "")
-
sopts, lopts = [], [], nil
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
left = [sopts.join(', ')]
-
right = desc.dup
-
-
while s = lopts.shift
-
l = left[-1].length + s.length
-
l += arg.length if left.size == 1 && arg
-
l < max or sopts.empty? or left << +''
-
left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
-
end
-
-
if arg
-
left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
-
end
-
mlen = left.collect {|ss| ss.length}.max.to_i
-
while mlen > width and l = left.shift
-
mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
-
if l.length < width and (r = right[0]) and !r.empty?
-
l = l.to_s.ljust(width) + ' ' + r
-
right.shift
-
end
-
yield(indent + l)
-
end
-
-
while begin l = left.shift; r = right.shift; l or r end
-
l = l.to_s.ljust(width) + ' ' + r if r and !r.empty?
-
yield(indent + l)
-
end
-
-
self
-
end
-
-
1
def add_banner(to) # :nodoc:
-
unless @short or @long
-
s = desc.join
-
to << " [" + s + "]..." unless s.empty?
-
end
-
to
-
end
-
-
1
def match_nonswitch?(str) # :nodoc:
-
@pattern =~ str unless @short or @long
-
end
-
-
#
-
# Main name of the switch.
-
#
-
1
def switch_name
-
(long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
-
end
-
-
1
def compsys(sdone, ldone) # :nodoc:
-
sopts, lopts = [], []
-
@short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
-
@long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
-
return if sopts.empty? and lopts.empty? # completely hidden
-
-
(sopts+lopts).each do |opt|
-
# "(-x -c -r)-l[left justify]"
-
if /^--\[no-\](.+)$/ =~ opt
-
o = $1
-
yield("--#{o}", desc.join(""))
-
yield("--no-#{o}", desc.join(""))
-
else
-
yield("#{opt}", desc.join(""))
-
end
-
end
-
end
-
-
#
-
# Switch that takes no arguments.
-
#
-
1
class NoArgument < self
-
-
#
-
# Raises an exception if any arguments given.
-
#
-
1
def parse(arg, argv)
-
yield(NeedlessArgument, arg) if arg
-
conv_arg(arg)
-
end
-
-
1
def self.incompatible_argument_styles(*)
-
end
-
-
1
def self.pattern
-
3
Object
-
end
-
end
-
-
#
-
# Switch that takes an argument.
-
#
-
1
class RequiredArgument < self
-
-
#
-
# Raises an exception if argument is not present.
-
#
-
1
def parse(arg, argv)
-
unless arg
-
raise MissingArgument if argv.empty?
-
arg = argv.shift
-
end
-
conv_arg(*parse_arg(arg, &method(:raise)))
-
end
-
end
-
-
#
-
# Switch that can omit argument.
-
#
-
1
class OptionalArgument < self
-
-
#
-
# Parses argument if given, or uses default value.
-
#
-
1
def parse(arg, argv, &error)
-
if arg
-
conv_arg(*parse_arg(arg, &error))
-
else
-
conv_arg(arg)
-
end
-
end
-
end
-
-
#
-
# Switch that takes an argument, which does not begin with '-'.
-
#
-
1
class PlacedArgument < self
-
-
#
-
# Returns nil if argument is not present or begins with '-'.
-
#
-
1
def parse(arg, argv, &error)
-
if !(val = arg) and (argv.empty? or /\A-/ =~ (val = argv[0]))
-
return nil, block, nil
-
end
-
opt = (val = parse_arg(val, &error))[1]
-
val = conv_arg(*val)
-
if opt and !arg
-
argv.shift
-
else
-
val[0] = nil
-
end
-
val
-
end
-
end
-
end
-
-
#
-
# Simple option list providing mapping from short and/or long option
-
# string to OptionParser::Switch and mapping from acceptable argument to
-
# matching pattern and converter pair. Also provides summary feature.
-
#
-
1
class List
-
# Map from acceptable argument types to pattern and converter pairs.
-
1
attr_reader :atype
-
-
# Map from short style option switches to actual switch objects.
-
1
attr_reader :short
-
-
# Map from long style option switches to actual switch objects.
-
1
attr_reader :long
-
-
# List of all switches and summary string.
-
1
attr_reader :list
-
-
#
-
# Just initializes all instance variables.
-
#
-
1
def initialize
-
3
@atype = {}
-
3
@short = OptionMap.new
-
3
@long = OptionMap.new
-
3
@list = []
-
end
-
-
#
-
# See OptionParser.accept.
-
#
-
1
def accept(t, pat = /.*/m, &block)
-
13
if pat
-
13
pat.respond_to?(:match) or
-
raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2))
-
else
-
pat = t if t.respond_to?(:match)
-
end
-
13
unless block
-
block = pat.method(:convert).to_proc if pat.respond_to?(:convert)
-
end
-
13
@atype[t] = [pat, block]
-
end
-
-
#
-
# See OptionParser.reject.
-
#
-
1
def reject(t)
-
@atype.delete(t)
-
end
-
-
#
-
# Adds +sw+ according to +sopts+, +lopts+ and +nlopts+.
-
#
-
# +sw+:: OptionParser::Switch instance to be added.
-
# +sopts+:: Short style option list.
-
# +lopts+:: Long style option list.
-
# +nlopts+:: Negated long style options list.
-
#
-
1
def update(sw, sopts, lopts, nsw = nil, nlopts = nil)
-
15
sopts.each {|o| @short[o] = sw} if sopts
-
16
lopts.each {|o| @long[o] = sw} if lopts
-
9
nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
-
9
used = @short.invert.update(@long.invert)
-
45
@list.delete_if {|o| Switch === o and !used[o]}
-
end
-
1
private :update
-
-
#
-
# Inserts +switch+ at the head of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# prepend(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def prepend(*args)
-
update(*args)
-
@list.unshift(args[0])
-
end
-
-
#
-
# Appends +switch+ at the tail of the list, and associates short, long
-
# and negated long options. Arguments are:
-
#
-
# +switch+:: OptionParser::Switch instance to be inserted.
-
# +short_opts+:: List of short style options.
-
# +long_opts+:: List of long style options.
-
# +nolong_opts+:: List of long style options with "no-" prefix.
-
#
-
# append(switch, short_opts, long_opts, nolong_opts)
-
#
-
1
def append(*args)
-
9
update(*args)
-
9
@list.push(args[0])
-
end
-
-
#
-
# Searches +key+ in +id+ list. The result is returned or yielded if a
-
# block is given. If it isn't found, nil is returned.
-
#
-
1
def search(id, key)
-
90
if list = __send__(id)
-
171
val = list.fetch(key) {return nil}
-
9
block_given? ? yield(val) : val
-
end
-
end
-
-
#
-
# Searches list +id+ for +opt+ and the optional patterns for completion
-
# +pat+. If +icase+ is true, the search is case insensitive. The result
-
# is returned or yielded if a block is given. If it isn't found, nil is
-
# returned.
-
#
-
1
def complete(id, opt, icase = false, *pat, &block)
-
__send__(id).complete(opt, icase, *pat, &block)
-
end
-
-
#
-
# Iterates over each option, passing the option to the +block+.
-
#
-
1
def each_option(&block)
-
list.each(&block)
-
end
-
-
#
-
# Creates the summary table, passing each line to the +block+ (without
-
# newline). The arguments +args+ are passed along to the summarize
-
# method which is called on every option.
-
#
-
1
def summarize(*args, &block)
-
sum = []
-
list.reverse_each do |opt|
-
if opt.respond_to?(:summarize) # perhaps OptionParser::Switch
-
s = []
-
opt.summarize(*args) {|l| s << l}
-
sum.concat(s.reverse)
-
elsif !opt or opt.empty?
-
sum << ""
-
elsif opt.respond_to?(:each_line)
-
sum.concat([*opt.each_line].reverse)
-
else
-
sum.concat([*opt.each].reverse)
-
end
-
end
-
sum.reverse_each(&block)
-
end
-
-
1
def add_banner(to) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:add_banner)
-
opt.add_banner(to)
-
end
-
end
-
to
-
end
-
-
1
def compsys(*args, &block) # :nodoc:
-
list.each do |opt|
-
if opt.respond_to?(:compsys)
-
opt.compsys(*args, &block)
-
end
-
end
-
end
-
end
-
-
#
-
# Hash with completion search feature. See OptionParser::Completion.
-
#
-
1
class CompletingHash < Hash
-
1
include Completion
-
-
#
-
# Completion for hash key.
-
#
-
1
def match(key)
-
*values = fetch(key) {
-
raise AmbiguousArgument, catch(:ambiguous) {return complete(key)}
-
}
-
return key, *values
-
end
-
end
-
-
# :stopdoc:
-
-
#
-
# Enumeration of acceptable argument styles. Possible values are:
-
#
-
# NO_ARGUMENT:: The switch takes no arguments. (:NONE)
-
# REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED)
-
# OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
-
#
-
# Use like --switch=argument (long style) or -Xargument (short style). For
-
# short style, only portion matched to argument pattern is treated as
-
# argument.
-
#
-
1
ArgumentStyle = {}
-
3
NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument}
-
3
RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument}
-
3
OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument}
-
1
ArgumentStyle.freeze
-
-
#
-
# Switches common used such as '--', and also provides default
-
# argument classes
-
#
-
1
DefaultList = List.new
-
1
DefaultList.short['-'] = Switch::NoArgument.new {}
-
1
DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
-
-
-
1
COMPSYS_HEADER = <<'XXX' # :nodoc:
-
-
typeset -A opt_args
-
local context state line
-
-
_arguments -s -S \
-
XXX
-
-
1
def compsys(to, name = File.basename($0)) # :nodoc:
-
to << "#compdef #{name}\n"
-
to << COMPSYS_HEADER
-
visit(:compsys, {}, {}) {|o, d|
-
to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n]
-
}
-
to << " '*:file:_files' && return 0\n"
-
end
-
-
#
-
# Default options for ARGV, which never appear in option summary.
-
#
-
1
Officious = {}
-
-
#
-
# --help
-
# Shows option summary.
-
#
-
1
Officious['help'] = proc do |parser|
-
1
Switch::NoArgument.new do |arg|
-
puts parser.help
-
exit
-
end
-
end
-
-
#
-
# --*-completion-bash=WORD
-
# Shows candidates for command line completion.
-
#
-
1
Officious['*-completion-bash'] = proc do |parser|
-
1
Switch::RequiredArgument.new do |arg|
-
puts parser.candidate(arg)
-
exit
-
end
-
end
-
-
#
-
# --*-completion-zsh[=NAME:FILE]
-
# Creates zsh completion file.
-
#
-
1
Officious['*-completion-zsh'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |arg|
-
parser.compsys(STDOUT, arg)
-
exit
-
end
-
end
-
-
#
-
# --version
-
# Shows version string if Version is defined.
-
#
-
1
Officious['version'] = proc do |parser|
-
1
Switch::OptionalArgument.new do |pkg|
-
if pkg
-
begin
-
require 'optparse/version'
-
rescue LoadError
-
else
-
show_version(*pkg.split(/,/)) or
-
abort("#{parser.program_name}: no version found in package #{pkg}")
-
exit
-
end
-
end
-
v = parser.ver or abort("#{parser.program_name}: version unknown")
-
puts v
-
exit
-
end
-
end
-
-
# :startdoc:
-
-
#
-
# Class methods
-
#
-
-
#
-
# Initializes a new instance and evaluates the optional block in context
-
# of the instance. Arguments +args+ are passed to #new, see there for
-
# description of parameters.
-
#
-
# This method is *deprecated*, its behavior corresponds to the older #new
-
# method.
-
#
-
1
def self.with(*args, &block)
-
opts = new(*args)
-
opts.instance_eval(&block)
-
opts
-
end
-
-
#
-
# Returns an incremented value of +default+ according to +arg+.
-
#
-
1
def self.inc(arg, default = nil)
-
case arg
-
when Integer
-
arg.nonzero?
-
when nil
-
default.to_i + 1
-
end
-
end
-
1
def inc(*args)
-
self.class.inc(*args)
-
end
-
-
#
-
# Initializes the instance and yields itself if called with a block.
-
#
-
# +banner+:: Banner message.
-
# +width+:: Summary width.
-
# +indent+:: Summary indent.
-
#
-
1
def initialize(banner = nil, width = 32, indent = ' ' * 4)
-
1
@stack = [DefaultList, List.new, List.new]
-
1
@program_name = nil
-
1
@banner = banner
-
1
@summary_width = width
-
1
@summary_indent = indent
-
1
@default_argv = ARGV
-
1
add_officious
-
1
yield self if block_given?
-
end
-
-
1
def add_officious # :nodoc:
-
1
list = base()
-
1
Officious.each do |opt, block|
-
4
list.long[opt] ||= block.call(self)
-
end
-
end
-
-
#
-
# Terminates option parsing. Optional parameter +arg+ is a string pushed
-
# back to be the first non-option argument.
-
#
-
1
def terminate(arg = nil)
-
self.class.terminate(arg)
-
end
-
1
def self.terminate(arg = nil)
-
throw :terminate, arg
-
end
-
-
1
@stack = [DefaultList]
-
14
def self.top() DefaultList end
-
-
#
-
# Directs to accept specified class +t+. The argument string is passed to
-
# the block in which it should be converted to the desired class.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
# +pat+:: Pattern for argument, defaults to +t+ if it responds to match.
-
#
-
# accept(t, pat, &block)
-
#
-
1
def accept(*args, &blk) top.accept(*args, &blk) end
-
#
-
# See #accept.
-
#
-
14
def self.accept(*args, &blk) top.accept(*args, &blk) end
-
-
#
-
# Directs to reject specified class argument.
-
#
-
# +t+:: Argument class specifier, any object including Class.
-
#
-
# reject(t)
-
#
-
1
def reject(*args, &blk) top.reject(*args, &blk) end
-
#
-
# See #reject.
-
#
-
1
def self.reject(*args, &blk) top.reject(*args, &blk) end
-
-
#
-
# Instance methods
-
#
-
-
# Heading banner preceding summary.
-
1
attr_writer :banner
-
-
# Program name to be emitted in error message and default banner,
-
# defaults to $0.
-
1
attr_writer :program_name
-
-
# Width for option list portion of summary. Must be Numeric.
-
1
attr_accessor :summary_width
-
-
# Indentation for summary. Must be String (or have + String method).
-
1
attr_accessor :summary_indent
-
-
# Strings to be parsed in default.
-
1
attr_accessor :default_argv
-
-
#
-
# Heading banner preceding summary.
-
#
-
1
def banner
-
unless @banner
-
@banner = +"Usage: #{program_name} [options]"
-
visit(:add_banner, @banner)
-
end
-
@banner
-
end
-
-
#
-
# Program name to be emitted in error message and default banner, defaults
-
# to $0.
-
#
-
1
def program_name
-
@program_name || File.basename($0, '.*')
-
end
-
-
# for experimental cascading :-)
-
1
alias set_banner banner=
-
1
alias set_program_name program_name=
-
1
alias set_summary_width summary_width=
-
1
alias set_summary_indent summary_indent=
-
-
# Version
-
1
attr_writer :version
-
# Release code
-
1
attr_writer :release
-
-
#
-
# Version
-
#
-
1
def version
-
(defined?(@version) && @version) || (defined?(::Version) && ::Version)
-
end
-
-
#
-
# Release code
-
#
-
1
def release
-
(defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
-
end
-
-
#
-
# Returns version string from program_name, version and release.
-
#
-
1
def ver
-
if v = version
-
str = +"#{program_name} #{[v].join('.')}"
-
str << " (#{v})" if v = release
-
str
-
end
-
end
-
-
1
def warn(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
1
def abort(mesg = $!)
-
super("#{program_name}: #{mesg}")
-
end
-
-
#
-
# Subject of #on / #on_head, #accept / #reject
-
#
-
1
def top
-
9
@stack[-1]
-
end
-
-
#
-
# Subject of #on_tail.
-
#
-
1
def base
-
1
@stack[1]
-
end
-
-
#
-
# Pushes a new List.
-
#
-
1
def new
-
@stack.push(List.new)
-
if block_given?
-
yield self
-
else
-
self
-
end
-
end
-
-
#
-
# Removes the last List.
-
#
-
1
def remove
-
@stack.pop
-
end
-
-
#
-
# Puts option summary into +to+ and returns +to+. Yields each line if
-
# a block is given.
-
#
-
# +to+:: Output destination, which must have method <<. Defaults to [].
-
# +width+:: Width of left side, defaults to @summary_width.
-
# +max+:: Maximum length allowed for left side, defaults to +width+ - 1.
-
# +indent+:: Indentation, defaults to @summary_indent.
-
#
-
1
def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
-
nl = "\n"
-
blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
-
visit(:summarize, {}, {}, width, max, indent, &blk)
-
to
-
end
-
-
#
-
# Returns option summary string.
-
#
-
1
def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
-
1
alias to_s help
-
-
#
-
# Returns option summary list.
-
#
-
1
def to_a; summarize("#{banner}".split(/^/)) end
-
-
#
-
# Checks if an argument is given twice, in which case an ArgumentError is
-
# raised. Called from OptionParser#switch only.
-
#
-
# +obj+:: New argument.
-
# +prv+:: Previously specified argument.
-
# +msg+:: Exception message.
-
#
-
1
def notwice(obj, prv, msg)
-
5
unless !prv or prv == obj
-
raise(ArgumentError, "argument #{msg} given twice: #{obj}",
-
ParseError.filter_backtrace(caller(2)))
-
end
-
5
obj
-
end
-
1
private :notwice
-
-
1
SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
-
#
-
# Creates an OptionParser::Switch from the parameters. The parsed argument
-
# value is passed to the given block, where it can be processed.
-
#
-
# See at the beginning of OptionParser for some full examples.
-
#
-
# +opts+ can include the following elements:
-
#
-
# [Argument style:]
-
# One of the following:
-
# :NONE, :REQUIRED, :OPTIONAL
-
#
-
# [Argument pattern:]
-
# Acceptable option argument format, must be pre-defined with
-
# OptionParser.accept or OptionParser#accept, or Regexp. This can appear
-
# once or assigned as String if not present, otherwise causes an
-
# ArgumentError. Examples:
-
# Float, Time, Array
-
#
-
# [Possible argument values:]
-
# Hash or Array.
-
# [:text, :binary, :auto]
-
# %w[iso-2022-jp shift_jis euc-jp utf8 binary]
-
# { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
-
#
-
# [Long style switch:]
-
# Specifies a long style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "--switch=MANDATORY" or "--switch MANDATORY"
-
# "--switch[=OPTIONAL]"
-
# "--switch"
-
#
-
# [Short style switch:]
-
# Specifies short style switch which takes a mandatory, optional or no
-
# argument. It's a string of the following form:
-
# "-xMANDATORY"
-
# "-x[OPTIONAL]"
-
# "-x"
-
# There is also a special form which matches character range (not full
-
# set of regular expression):
-
# "-[a-z]MANDATORY"
-
# "-[a-z][OPTIONAL]"
-
# "-[a-z]"
-
#
-
# [Argument style and description:]
-
# Instead of specifying mandatory or optional arguments directly in the
-
# switch parameter, this separate parameter can be used.
-
# "=MANDATORY"
-
# "=[OPTIONAL]"
-
#
-
# [Description:]
-
# Description string for the option.
-
# "Run verbosely"
-
# If you give multiple description strings, each string will be printed
-
# line by line.
-
#
-
# [Handler:]
-
# Handler for the parsed argument value. Either give a block or pass a
-
# Proc or Method as an argument.
-
#
-
1
def make_switch(opts, block = nil)
-
7
short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
-
7
ldesc, sdesc, desc, arg = [], [], []
-
7
default_style = Switch::NoArgument
-
7
default_pattern = nil
-
7
klass = nil
-
7
q, a = nil
-
7
has_arg = false
-
-
7
opts.each do |o|
-
# argument class
-
21
next if search(:atype, o) do |pat, c|
-
1
klass = notwice(o, klass, 'type')
-
1
if not_style and not_style != Switch::NoArgument
-
not_pattern, not_conv = pat, c
-
else
-
1
default_pattern, conv = pat, c
-
end
-
end
-
-
# directly specified pattern(any object possible to match)
-
20
if (!(String === o || Symbol === o)) and o.respond_to?(:match)
-
pattern = notwice(o, pattern, 'pattern')
-
if pattern.respond_to?(:convert)
-
conv = pattern.method(:convert).to_proc
-
else
-
conv = SPLAT_PROC
-
end
-
next
-
end
-
-
# anything others
-
20
case o
-
when Proc, Method
-
block = notwice(o, block, 'block')
-
when Array, Hash
-
case pattern
-
when CompletingHash
-
when nil
-
pattern = CompletingHash.new
-
conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
-
else
-
raise ArgumentError, "argument pattern given twice"
-
end
-
o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
-
when Module
-
raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
-
when *ArgumentStyle.keys
-
style = notwice(ArgumentStyle[o], style, 'style')
-
when /^--no-([^\[\]=\s]*)(.+)?/
-
1
q, a = $1, $2
-
1
o = notwice(a ? Object : TrueClass, klass, 'type')
-
1
not_pattern, not_conv = search(:atype, o) unless not_style
-
1
not_style = (not_style || default_style).guess(arg = a) if a
-
1
default_style = Switch::NoArgument
-
1
default_pattern, conv = search(:atype, FalseClass) unless default_pattern
-
1
ldesc << "--no-#{q}"
-
1
(q = q.downcase).tr!('_', '-')
-
1
long << "no-#{q}"
-
1
nolong << q
-
when /^--\[no-\]([^\[\]=\s]*)(.+)?/
-
q, a = $1, $2
-
o = notwice(a ? Object : TrueClass, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
ldesc << "--[no-]#{q}"
-
(o = q.downcase).tr!('_', '-')
-
long << o
-
not_pattern, not_conv = search(:atype, FalseClass) unless not_style
-
not_style = Switch::NoArgument
-
nolong << "no-#{o}"
-
when /^--([^\[\]=\s]*)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
3
o = notwice(NilClass, klass, 'type')
-
3
default_style = default_style.guess(arg = a)
-
3
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
ldesc << "--#{q}"
-
6
(o = q.downcase).tr!('_', '-')
-
6
long << o
-
when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
-
q, a = $1, $2
-
o = notwice(Object, klass, 'type')
-
if a
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
else
-
has_arg = true
-
end
-
sdesc << "-#{q}"
-
short << Regexp.new(q)
-
when /^-(.)(.+)?/
-
6
q, a = $1, $2
-
6
if a
-
o = notwice(NilClass, klass, 'type')
-
default_style = default_style.guess(arg = a)
-
default_pattern, conv = search(:atype, o) unless default_pattern
-
end
-
6
sdesc << "-#{q}"
-
6
short << q
-
when /^=/
-
style = notwice(default_style.guess(arg = o), style, 'style')
-
default_pattern, conv = search(:atype, Object) unless default_pattern
-
else
-
7
desc.push(o)
-
end
-
end
-
-
7
default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
-
7
if !(short.empty? and long.empty?)
-
7
if has_arg and default_style == Switch::NoArgument
-
default_style = Switch::RequiredArgument
-
end
-
7
s = (style || default_style).new(pattern || default_pattern,
-
conv, sdesc, ldesc, arg, desc, block)
-
elsif !block
-
if style or pattern
-
raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
-
end
-
s = desc
-
else
-
short << pattern
-
s = (style || default_style).new(pattern,
-
conv, nil, nil, arg, desc, block)
-
end
-
7
return s, short, long,
-
7
(not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
-
nolong
-
end
-
-
1
def define(*opts, &block)
-
7
top.append(*(sw = make_switch(opts, block)))
-
7
sw[0]
-
end
-
-
#
-
# Add option switch and handler. See #make_switch for an explanation of
-
# parameters.
-
#
-
1
def on(*opts, &block)
-
7
define(*opts, &block)
-
7
self
-
end
-
1
alias def_option define
-
-
1
def define_head(*opts, &block)
-
top.prepend(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at head of summary.
-
#
-
1
def on_head(*opts, &block)
-
define_head(*opts, &block)
-
self
-
end
-
1
alias def_head_option define_head
-
-
1
def define_tail(*opts, &block)
-
base.append(*(sw = make_switch(opts, block)))
-
sw[0]
-
end
-
-
#
-
# Add option switch like with #on, but at tail of summary.
-
#
-
1
def on_tail(*opts, &block)
-
define_tail(*opts, &block)
-
self
-
end
-
1
alias def_tail_option define_tail
-
-
#
-
# Add separator in summary.
-
#
-
1
def separator(string)
-
2
top.append(string, nil, nil)
-
end
-
-
#
-
# Parses command line arguments +argv+ in order. When a block is given,
-
# each non-option argument is yielded.
-
#
-
# Returns the rest of +argv+ left unparsed.
-
#
-
1
def order(*argv, into: nil, &nonopt)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
order!(argv, into: into, &nonopt)
-
end
-
-
#
-
# Same as #order, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def order!(argv = default_argv, into: nil, &nonopt)
-
1
setter = ->(name, val) {into[name.to_sym] = val} if into
-
1
parse_in_order(argv, setter, &nonopt)
-
end
-
-
1
def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc:
-
1
opt, arg, val, rest = nil
-
1
nonopt ||= proc {|a| throw :terminate, a}
-
1
argv.unshift(arg) if arg = catch(:terminate) {
-
1
while arg = argv.shift
-
case arg
-
# long option
-
when /\A--([^=]*)(?:=(.*))?/m
-
opt, rest = $1, $2
-
opt.tr!('_', '-')
-
begin
-
sw, = complete(:long, opt, true)
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, rest)
-
end
-
-
# short option
-
when /\A-(.)((=).*|.+)?/m
-
eq, rest, opt = $3, $2, $1
-
has_arg, val = eq, rest
-
begin
-
sw, = search(:short, opt)
-
unless sw
-
begin
-
sw, = complete(:short, opt)
-
# short option matched.
-
val = arg.delete_prefix('-')
-
has_arg = true
-
rescue InvalidOption
-
# if no short options match, try completion with long
-
# options.
-
sw, = complete(:long, opt)
-
eq ||= !rest
-
end
-
end
-
rescue ParseError
-
raise $!.set_option(arg, true)
-
end
-
begin
-
opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
-
raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
-
argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
-
val = cb.call(val) if cb
-
setter.call(sw.switch_name, val) if setter
-
rescue ParseError
-
raise $!.set_option(arg, arg.length > 2)
-
end
-
-
# non-option argument
-
else
-
catch(:prune) do
-
visit(:each_option) do |sw0|
-
sw = sw0
-
sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg)
-
end
-
nonopt.call(arg)
-
end
-
end
-
end
-
-
1
nil
-
}
-
-
1
visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern}
-
-
1
argv
-
end
-
1
private :parse_in_order
-
-
#
-
# Parses command line arguments +argv+ in permutation mode and returns
-
# list of non-option arguments.
-
#
-
1
def permute(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
permute!(argv, into: into)
-
end
-
-
#
-
# Same as #permute, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def permute!(argv = default_argv, into: nil)
-
1
nonopts = []
-
1
order!(argv, into: into, &nonopts.method(:<<))
-
1
argv[0, 0] = nonopts
-
1
argv
-
end
-
-
#
-
# Parses command line arguments +argv+ in order when environment variable
-
# POSIXLY_CORRECT is set, and in permutation mode otherwise.
-
#
-
1
def parse(*argv, into: nil)
-
argv = argv[0].dup if argv.size == 1 and Array === argv[0]
-
parse!(argv, into: into)
-
end
-
-
#
-
# Same as #parse, but removes switches destructively.
-
# Non-option arguments remain in +argv+.
-
#
-
1
def parse!(argv = default_argv, into: nil)
-
1
if ENV.include?('POSIXLY_CORRECT')
-
order!(argv, into: into)
-
else
-
1
permute!(argv, into: into)
-
end
-
end
-
-
#
-
# Wrapper method for getopts.rb.
-
#
-
# params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
-
# # params["a"] = true # -a
-
# # params["b"] = "1" # -b1
-
# # params["foo"] = "1" # --foo
-
# # params["bar"] = "x" # --bar x
-
# # params["zot"] = "z" # --zot Z
-
#
-
1
def getopts(*args)
-
argv = Array === args.first ? args.shift : default_argv
-
single_options, *long_options = *args
-
-
result = {}
-
-
single_options.scan(/(.)(:)?/) do |opt, val|
-
if val
-
result[opt] = nil
-
define("-#{opt} VAL")
-
else
-
result[opt] = false
-
define("-#{opt}")
-
end
-
end if single_options
-
-
long_options.each do |arg|
-
arg, desc = arg.split(';', 2)
-
opt, val = arg.split(':', 2)
-
if val
-
result[opt] = val.empty? ? nil : val
-
define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
-
else
-
result[opt] = false
-
define("--#{opt}", *[desc].compact)
-
end
-
end
-
-
parse_in_order(argv, result.method(:[]=))
-
result
-
end
-
-
#
-
# See #getopts.
-
#
-
1
def self.getopts(*args)
-
new.getopts(*args)
-
end
-
-
#
-
# Traverses @stack, sending each element method +id+ with +args+ and
-
# +block+.
-
#
-
1
def visit(id, *args, &block)
-
30
@stack.reverse_each do |el|
-
90
el.send(id, *args, &block)
-
end
-
nil
-
end
-
1
private :visit
-
-
#
-
# Searches +key+ in @stack for +id+ hash and returns or yields the result.
-
#
-
1
def search(id, key)
-
29
block_given = block_given?
-
29
visit(:search, id, key) do |k|
-
9
return block_given ? yield(k) : k
-
end
-
end
-
1
private :search
-
-
#
-
# Completes shortened long style option switch and returns pair of
-
# canonical switch and switch descriptor OptionParser::Switch.
-
#
-
# +typ+:: Searching table.
-
# +opt+:: Searching key.
-
# +icase+:: Search case insensitive if true.
-
# +pat+:: Optional pattern for completion.
-
#
-
1
def complete(typ, opt, icase = false, *pat)
-
if pat.empty?
-
search(typ, opt) {|sw| return [sw, opt]} # exact match or...
-
end
-
raise AmbiguousOption, catch(:ambiguous) {
-
visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
-
raise InvalidOption, opt
-
}
-
end
-
1
private :complete
-
-
1
def candidate(word)
-
list = []
-
case word
-
when '-'
-
long = short = true
-
when /\A--/
-
word, arg = word.split(/=/, 2)
-
argpat = Completion.regexp(arg, false) if arg and !arg.empty?
-
long = true
-
when /\A-/
-
short = true
-
end
-
pat = Completion.regexp(word, long)
-
visit(:each_option) do |opt|
-
next unless Switch === opt
-
opts = (long ? opt.long : []) + (short ? opt.short : [])
-
opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
-
if /\A=/ =~ opt.arg
-
opts.map! {|sw| sw + "="}
-
if arg and CompletingHash === opt.pattern
-
if opts = opt.pattern.candidate(arg, false, argpat)
-
opts.map!(&:last)
-
end
-
end
-
end
-
list.concat(opts)
-
end
-
list
-
end
-
-
#
-
# Loads options from file names as +filename+. Does nothing when the file
-
# is not present. Returns whether successfully loaded.
-
#
-
# +filename+ defaults to basename of the program without suffix in a
-
# directory ~/.options.
-
#
-
1
def load(filename = nil)
-
begin
-
filename ||= File.expand_path(File.basename($0, '.*'), '~/.options')
-
rescue
-
return false
-
end
-
begin
-
parse(*IO.readlines(filename).each {|s| s.chomp!})
-
true
-
rescue Errno::ENOENT, Errno::ENOTDIR
-
false
-
end
-
end
-
-
#
-
# Parses environment variable +env+ or its uppercase with splitting like a
-
# shell.
-
#
-
# +env+ defaults to the basename of the program.
-
#
-
1
def environment(env = File.basename($0, '.*'))
-
env = ENV[env] || ENV[env.upcase] or return
-
require 'shellwords'
-
parse(*Shellwords.shellwords(env))
-
end
-
-
#
-
# Acceptable argument classes
-
#
-
-
#
-
# Any string and no conversion. This is fall-back.
-
#
-
1
accept(Object) {|s,|s or s.nil?}
-
-
1
accept(NilClass) {|s,|s}
-
-
#
-
# Any non-empty string, and no conversion.
-
#
-
1
accept(String, /.+/m) {|s,*|s}
-
-
#
-
# Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
-
# for 0x, and decimal for others; with optional sign prefix. Converts to
-
# Integer.
-
#
-
1
decimal = '\d+(?:_\d+)*'
-
1
binary = 'b[01]+(?:_[01]+)*'
-
1
hex = 'x[\da-f]+(?:_[\da-f]+)*'
-
1
octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
-
1
integer = "#{octal}|#{decimal}"
-
-
1
accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
-
begin
-
Integer(s)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Float number format, and converts to Float.
-
#
-
1
float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
-
1
floatpat = %r"\A[-+]?#{float}\z"io
-
1
accept(Float, floatpat) {|s,| s.to_f if s}
-
-
#
-
# Generic numeric format, converts to Integer for integer format, Float
-
# for float format, and Rational for rational format.
-
#
-
1
real = "[-+]?(?:#{octal}|#{float})"
-
1
accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
-
if n
-
Rational(d, n)
-
elsif f
-
Float(s)
-
else
-
Integer(s)
-
end
-
}
-
-
#
-
# Decimal integer format, to be converted to Integer.
-
#
-
1
DecimalInteger = /\A[-+]?#{decimal}\z/io
-
1
accept(DecimalInteger, DecimalInteger) {|s,|
-
begin
-
Integer(s, 10)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Ruby/C like octal/hexadecimal/binary integer format, to be converted to
-
# Integer.
-
#
-
1
OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
-
1
accept(OctalInteger, OctalInteger) {|s,|
-
begin
-
Integer(s, 8)
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Decimal integer/float number format, to be converted to Integer for
-
# integer format, Float for float format.
-
#
-
1
DecimalNumeric = floatpat # decimal integer is allowed as float also.
-
1
accept(DecimalNumeric, floatpat) {|s, f|
-
begin
-
if f
-
Float(s)
-
else
-
Integer(s)
-
end
-
rescue ArgumentError
-
raise OptionParser::InvalidArgument, s
-
end if s
-
}
-
-
#
-
# Boolean switch, which means whether it is present or not, whether it is
-
# absent or not with prefix no-, or it takes an argument
-
# yes/no/true/false/+/-.
-
#
-
1
yesno = CompletingHash.new
-
4
%w[- no false].each {|el| yesno[el] = false}
-
4
%w[+ yes true].each {|el| yesno[el] = true}
-
1
yesno['nil'] = false # should be nil?
-
1
accept(TrueClass, yesno) {|arg, val| val == nil or val}
-
#
-
# Similar to TrueClass, but defaults to false.
-
#
-
1
accept(FalseClass, yesno) {|arg, val| val != nil and val}
-
-
#
-
# List of strings separated by ",".
-
#
-
1
accept(Array) do |s, |
-
if s
-
s = s.split(',').collect {|ss| ss unless ss.empty?}
-
end
-
s
-
end
-
-
#
-
# Regular expression with options.
-
#
-
1
accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o|
-
f = 0
-
if o
-
f |= Regexp::IGNORECASE if /i/ =~ o
-
f |= Regexp::MULTILINE if /m/ =~ o
-
f |= Regexp::EXTENDED if /x/ =~ o
-
k = o.delete("imx")
-
k = nil if k.empty?
-
end
-
Regexp.new(s || all, f, k)
-
end
-
-
#
-
# Exceptions
-
#
-
-
#
-
# Base class of exceptions from OptionParser.
-
#
-
1
class ParseError < RuntimeError
-
# Reason which caused the error.
-
1
Reason = 'parse error'
-
-
1
def initialize(*args)
-
@args = args
-
@reason = nil
-
end
-
-
1
attr_reader :args
-
1
attr_writer :reason
-
-
#
-
# Pushes back erred argument(s) to +argv+.
-
#
-
1
def recover(argv)
-
argv[0, 0] = @args
-
argv
-
end
-
-
1
def self.filter_backtrace(array)
-
unless $DEBUG
-
array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~))
-
end
-
array
-
end
-
-
1
def set_backtrace(array)
-
super(self.class.filter_backtrace(array))
-
end
-
-
1
def set_option(opt, eq)
-
if eq
-
@args[0] = opt
-
else
-
@args.unshift(opt)
-
end
-
self
-
end
-
-
#
-
# Returns error reason. Override this for I18N.
-
#
-
1
def reason
-
@reason || self.class::Reason
-
end
-
-
1
def inspect
-
"#<#{self.class}: #{args.join(' ')}>"
-
end
-
-
#
-
# Default stringizing method to emit standard error message.
-
#
-
1
def message
-
reason + ': ' + args.join(' ')
-
end
-
-
1
alias to_s message
-
end
-
-
#
-
# Raises when ambiguously completable string is encountered.
-
#
-
1
class AmbiguousOption < ParseError
-
1
const_set(:Reason, 'ambiguous option')
-
end
-
-
#
-
# Raises when there is an argument for a switch which takes no argument.
-
#
-
1
class NeedlessArgument < ParseError
-
1
const_set(:Reason, 'needless argument')
-
end
-
-
#
-
# Raises when a switch with mandatory argument has no argument.
-
#
-
1
class MissingArgument < ParseError
-
1
const_set(:Reason, 'missing argument')
-
end
-
-
#
-
# Raises when switch is undefined.
-
#
-
1
class InvalidOption < ParseError
-
1
const_set(:Reason, 'invalid option')
-
end
-
-
#
-
# Raises when the given argument does not match required format.
-
#
-
1
class InvalidArgument < ParseError
-
1
const_set(:Reason, 'invalid argument')
-
end
-
-
#
-
# Raises when the given argument word can't be completed uniquely.
-
#
-
1
class AmbiguousArgument < InvalidArgument
-
1
const_set(:Reason, 'ambiguous argument')
-
end
-
-
#
-
# Miscellaneous
-
#
-
-
#
-
# Extends command line arguments array (ARGV) to parse itself.
-
#
-
1
module Arguable
-
-
#
-
# Sets OptionParser object, when +opt+ is +false+ or +nil+, methods
-
# OptionParser::Arguable#options and OptionParser::Arguable#options= are
-
# undefined. Thus, there is no ways to access the OptionParser object
-
# via the receiver object.
-
#
-
1
def options=(opt)
-
unless @optparse = opt
-
class << self
-
undef_method(:options)
-
undef_method(:options=)
-
end
-
end
-
end
-
-
#
-
# Actual OptionParser object, automatically created if nonexistent.
-
#
-
# If called with a block, yields the OptionParser object and returns the
-
# result of the block. If an OptionParser::ParseError exception occurs
-
# in the block, it is rescued, a error message printed to STDERR and
-
# +nil+ returned.
-
#
-
1
def options
-
@optparse ||= OptionParser.new
-
@optparse.default_argv = self
-
block_given? or return @optparse
-
begin
-
yield @optparse
-
rescue ParseError
-
@optparse.warn $!
-
nil
-
end
-
end
-
-
#
-
# Parses +self+ destructively in order and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def order!(&blk) options.order!(self, &blk) end
-
-
#
-
# Parses +self+ destructively in permutation mode and returns +self+
-
# containing the rest arguments left unparsed.
-
#
-
1
def permute!() options.permute!(self) end
-
-
#
-
# Parses +self+ destructively and returns +self+ containing the
-
# rest arguments left unparsed.
-
#
-
1
def parse!() options.parse!(self) end
-
-
#
-
# Substitution of getopts is possible as follows. Also see
-
# OptionParser#getopts.
-
#
-
# def getopts(*args)
-
# ($OPT = ARGV.getopts(*args)).each do |opt, val|
-
# eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val"
-
# end
-
# rescue OptionParser::ParseError
-
# end
-
#
-
1
def getopts(*args)
-
options.getopts(self, *args)
-
end
-
-
#
-
# Initializes instance variable.
-
#
-
1
def self.extend_object(obj)
-
1
super
-
2
obj.instance_eval {@optparse = nil}
-
end
-
1
def initialize(*args)
-
super
-
@optparse = nil
-
end
-
end
-
-
#
-
# Acceptable argument classes. Now contains DecimalInteger, OctalInteger
-
# and DecimalNumeric. See Acceptable argument classes (in source code).
-
#
-
1
module Acceptables
-
1
const_set(:DecimalInteger, OptionParser::DecimalInteger)
-
1
const_set(:OctalInteger, OptionParser::OctalInteger)
-
1
const_set(:DecimalNumeric, OptionParser::DecimalNumeric)
-
end
-
end
-
-
# ARGV is arguable by OptionParser
-
1
ARGV.extend(OptionParser::Arguable)
-
-
# An alias for OptionParser.
-
1
OptParse = OptionParser # :nodoc:
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
#
-
# Example using a Gem::Package
-
#
-
# Builds a .gem file given a Gem::Specification. A .gem file is a tarball
-
# which contains a data.tar.gz and metadata.gz, and possibly signatures.
-
#
-
# require 'rubygems'
-
# require 'rubygems/package'
-
#
-
# spec = Gem::Specification.new do |s|
-
# s.summary = "Ruby based make-like utility."
-
# s.name = 'rake'
-
# s.version = PKG_VERSION
-
# s.requirements << 'none'
-
# s.files = PKG_FILES
-
# s.description = <<-EOF
-
# Rake is a Make-like program implemented in Ruby. Tasks
-
# and dependencies are specified in standard Ruby syntax.
-
# EOF
-
# end
-
#
-
# Gem::Package.build spec
-
#
-
# Reads a .gem file.
-
#
-
# require 'rubygems'
-
# require 'rubygems/package'
-
#
-
# the_gem = Gem::Package.new(path_to_dot_gem)
-
# the_gem.contents # get the files in the gem
-
# the_gem.extract_files destination_directory # extract the gem into a directory
-
# the_gem.spec # get the spec out of the gem
-
# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive)
-
#
-
# #files are the files in the .gem tar file, not the Ruby files in the gem
-
# #extract_files and #contents automatically call #verify
-
-
1
require 'rubygems/security'
-
1
require 'rubygems/specification'
-
1
require 'rubygems/user_interaction'
-
1
require 'zlib'
-
-
1
class Gem::Package
-
-
1
include Gem::UserInteraction
-
-
1
class Error < Gem::Exception; end
-
-
1
class FormatError < Error
-
1
attr_reader :path
-
-
1
def initialize(message, source = nil)
-
if source
-
@path = source.path
-
-
message = message + " in #{path}" if path
-
end
-
-
super message
-
end
-
-
end
-
-
1
class PathError < Error
-
1
def initialize(destination, destination_dir)
-
super "installing into parent path %s of %s is not allowed" %
-
[destination, destination_dir]
-
end
-
end
-
-
1
class NonSeekableIO < Error; end
-
-
1
class TooLongFileName < Error; end
-
-
##
-
# Raised when a tar file is corrupt
-
-
1
class TarInvalidError < Error; end
-
-
-
1
attr_accessor :build_time # :nodoc:
-
-
##
-
# Checksums for the contents of the package
-
-
1
attr_reader :checksums
-
-
##
-
# The files in this package. This is not the contents of the gem, just the
-
# files in the top-level container.
-
-
1
attr_reader :files
-
-
##
-
# The security policy used for verifying the contents of this package.
-
-
1
attr_accessor :security_policy
-
-
##
-
# Sets the Gem::Specification to use to build this package.
-
-
1
attr_writer :spec
-
-
##
-
# Permission for directories
-
1
attr_accessor :dir_mode
-
-
##
-
# Permission for program files
-
1
attr_accessor :prog_mode
-
-
##
-
# Permission for other files
-
1
attr_accessor :data_mode
-
-
1
def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
-
gem_file = file_name || spec.file_name
-
-
package = new gem_file
-
package.spec = spec
-
package.build skip_validation, strict_validation
-
-
gem_file
-
end
-
-
##
-
# Creates a new Gem::Package for the file at +gem+. +gem+ can also be
-
# provided as an IO object.
-
#
-
# If +gem+ is an existing file in the old format a Gem::Package::Old will be
-
# returned.
-
-
1
def self.new(gem, security_policy = nil)
-
gem = if gem.is_a?(Gem::Package::Source)
-
gem
-
elsif gem.respond_to? :read
-
Gem::Package::IOSource.new gem
-
else
-
Gem::Package::FileSource.new gem
-
end
-
-
return super unless Gem::Package == self
-
return super unless gem.present?
-
-
return super unless gem.start
-
return super unless gem.start.include? 'MD5SUM ='
-
-
Gem::Package::Old.new gem
-
end
-
-
##
-
# Creates a new package that will read or write to the file +gem+.
-
-
1
def initialize(gem, security_policy) # :notnew:
-
@gem = gem
-
-
@build_time = ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
-
@checksums = {}
-
@contents = nil
-
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
-
@files = nil
-
@security_policy = security_policy
-
@signatures = {}
-
@signer = nil
-
@spec = nil
-
end
-
-
##
-
# Copies this package to +path+ (if possible)
-
-
1
def copy_to(path)
-
FileUtils.cp @gem.path, path unless File.exist? path
-
end
-
-
##
-
# Adds a checksum for each entry in the gem to checksums.yaml.gz.
-
-
1
def add_checksums(tar)
-
Gem.load_yaml
-
-
checksums_by_algorithm = Hash.new { |h, algorithm| h[algorithm] = {} }
-
-
@checksums.each do |name, digests|
-
digests.each do |algorithm, digest|
-
checksums_by_algorithm[algorithm][name] = digest.hexdigest
-
end
-
end
-
-
tar.add_file_signed 'checksums.yaml.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
YAML.dump checksums_by_algorithm, gz_io
-
end
-
end
-
end
-
-
##
-
# Adds the files listed in the packages's Gem::Specification to data.tar.gz
-
# and adds this file to the +tar+.
-
-
1
def add_contents(tar) # :nodoc:
-
digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
Gem::Package::TarWriter.new gz_io do |data_tar|
-
add_files data_tar
-
end
-
end
-
end
-
-
@checksums['data.tar.gz'] = digests
-
end
-
-
##
-
# Adds files included the package's Gem::Specification to the +tar+ file
-
-
1
def add_files(tar) # :nodoc:
-
@spec.files.each do |file|
-
stat = File.lstat file
-
-
if stat.symlink?
-
target_path = File.readlink(file)
-
-
unless target_path.start_with? '.'
-
relative_dir = File.dirname(file).sub("#{Dir.pwd}/", '')
-
target_path = File.join(relative_dir, target_path)
-
end
-
-
tar.add_symlink file, target_path, stat.mode
-
end
-
-
next unless stat.file?
-
-
tar.add_file_simple file, stat.mode, stat.size do |dst_io|
-
File.open file, 'rb' do |src_io|
-
dst_io.write src_io.read 16384 until src_io.eof?
-
end
-
end
-
end
-
end
-
-
##
-
# Adds the package's Gem::Specification to the +tar+ file
-
-
1
def add_metadata(tar) # :nodoc:
-
digests = tar.add_file_signed 'metadata.gz', 0444, @signer do |io|
-
gzip_to io do |gz_io|
-
gz_io.write @spec.to_yaml
-
end
-
end
-
-
@checksums['metadata.gz'] = digests
-
end
-
-
##
-
# Builds this package based on the specification set by #spec=
-
-
1
def build(skip_validation = false, strict_validation = false)
-
raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation
-
-
Gem.load_yaml
-
require 'rubygems/security'
-
-
@spec.mark_version
-
@spec.validate true, strict_validation unless skip_validation
-
-
setup_signer(
-
signer_options: {
-
expiration_length_days: Gem.configuration.cert_expiration_length_days
-
}
-
)
-
-
@gem.with_write_io do |gem_io|
-
Gem::Package::TarWriter.new gem_io do |gem|
-
add_metadata gem
-
add_contents gem
-
add_checksums gem
-
end
-
end
-
-
say <<-EOM
-
Successfully built RubyGem
-
Name: #{@spec.name}
-
Version: #{@spec.version}
-
File: #{File.basename @gem.path}
-
EOM
-
ensure
-
@signer = nil
-
end
-
-
##
-
# A list of file names contained in this gem
-
-
1
def contents
-
return @contents if @contents
-
-
verify unless @spec
-
-
@contents = []
-
-
@gem.with_read_io do |io|
-
gem_tar = Gem::Package::TarReader.new io
-
-
gem_tar.each do |entry|
-
next unless entry.full_name == 'data.tar.gz'
-
-
open_tar_gz entry do |pkg_tar|
-
pkg_tar.each do |contents_entry|
-
@contents << contents_entry.full_name
-
end
-
end
-
-
return @contents
-
end
-
end
-
end
-
-
##
-
# Creates a digest of the TarEntry +entry+ from the digest algorithm set by
-
# the security policy.
-
-
1
def digest(entry) # :nodoc:
-
algorithms = if @checksums
-
@checksums.keys
-
else
-
[Gem::Security::DIGEST_NAME].compact
-
end
-
-
algorithms.each do |algorithm|
-
digester =
-
if defined?(OpenSSL::Digest)
-
OpenSSL::Digest.new algorithm
-
else
-
Digest.const_get(algorithm).new
-
end
-
-
digester << entry.read(16384) until entry.eof?
-
-
entry.rewind
-
-
@digests[algorithm][entry.full_name] = digester
-
end
-
-
@digests
-
end
-
-
##
-
# Extracts the files in this package into +destination_dir+
-
#
-
# If +pattern+ is specified, only entries matching that glob will be
-
# extracted.
-
-
1
def extract_files(destination_dir, pattern = "*")
-
verify unless @spec
-
-
FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755
-
-
@gem.with_read_io do |io|
-
reader = Gem::Package::TarReader.new io
-
-
reader.each do |entry|
-
next unless entry.full_name == 'data.tar.gz'
-
-
extract_tar_gz entry, destination_dir, pattern
-
-
return # ignore further entries
-
end
-
end
-
end
-
-
##
-
# Extracts all the files in the gzipped tar archive +io+ into
-
# +destination_dir+.
-
#
-
# If an entry in the archive contains a relative path above
-
# +destination_dir+ or an absolute path is encountered an exception is
-
# raised.
-
#
-
# If +pattern+ is specified, only entries matching that glob will be
-
# extracted.
-
-
1
def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
-
directories = [] if dir_mode
-
open_tar_gz io do |tar|
-
tar.each do |entry|
-
next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH
-
-
destination = install_location entry.full_name, destination_dir
-
-
FileUtils.rm_rf destination
-
-
mkdir_options = {}
-
mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?)
-
mkdir =
-
if entry.directory?
-
destination
-
else
-
File.dirname destination
-
end
-
directories << mkdir if directories
-
-
mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name
-
-
File.open destination, 'wb' do |out|
-
out.write entry.read
-
FileUtils.chmod file_mode(entry.header.mode), destination
-
end if entry.file?
-
-
File.symlink(entry.header.linkname, destination) if entry.symlink?
-
-
verbose destination
-
end
-
end
-
-
if directories
-
directories.uniq!
-
File.chmod(dir_mode, *directories)
-
end
-
end
-
-
1
def file_mode(mode) # :nodoc:
-
((mode & 0111).zero? ? data_mode : prog_mode) || mode
-
end
-
-
##
-
# Gzips content written to +gz_io+ to +io+.
-
#--
-
# Also sets the gzip modification time to the package build time to ease
-
# testing.
-
-
1
def gzip_to(io) # :yields: gz_io
-
gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION
-
gz_io.mtime = @build_time
-
-
yield gz_io
-
ensure
-
gz_io.close
-
end
-
-
##
-
# Returns the full path for installing +filename+.
-
#
-
# If +filename+ is not inside +destination_dir+ an exception is raised.
-
-
1
def install_location(filename, destination_dir) # :nodoc:
-
raise Gem::Package::PathError.new(filename, destination_dir) if
-
filename.start_with? '/'
-
-
destination_dir = File.expand_path(File.realpath(destination_dir))
-
destination = File.expand_path(File.join(destination_dir, filename))
-
-
raise Gem::Package::PathError.new(destination, destination_dir) unless
-
destination.start_with? destination_dir + '/'
-
-
begin
-
real_destination = File.expand_path(File.realpath(destination))
-
rescue
-
# it's fine if the destination doesn't exist, because rm -rf'ing it can't cause any damage
-
nil
-
else
-
raise Gem::Package::PathError.new(real_destination, destination_dir) unless
-
real_destination.start_with? destination_dir + '/'
-
end
-
-
destination.untaint
-
destination
-
end
-
-
1
def normalize_path(pathname)
-
if Gem.win_platform?
-
pathname.downcase
-
else
-
pathname
-
end
-
end
-
-
1
def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name)
-
destination_dir = File.realpath(File.expand_path(destination_dir))
-
parts = mkdir.split(File::SEPARATOR)
-
parts.reduce do |path, basename|
-
path = File.realpath(path) unless path == ""
-
path = File.expand_path(path + File::SEPARATOR + basename)
-
lstat = File.lstat path rescue nil
-
if !lstat || !lstat.directory?
-
unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, mkdir_options rescue false)
-
raise Gem::Package::PathError.new(file_name, destination_dir)
-
end
-
end
-
path
-
end
-
end
-
-
##
-
# Loads a Gem::Specification from the TarEntry +entry+
-
-
1
def load_spec(entry) # :nodoc:
-
case entry.full_name
-
when 'metadata' then
-
@spec = Gem::Specification.from_yaml entry.read
-
when 'metadata.gz' then
-
args = [entry]
-
args << { :external_encoding => Encoding::UTF_8 } if
-
Zlib::GzipReader.method(:wrap).arity != 1
-
-
Zlib::GzipReader.wrap(*args) do |gzio|
-
@spec = Gem::Specification.from_yaml gzio.read
-
end
-
end
-
end
-
-
##
-
# Opens +io+ as a gzipped tar archive
-
-
1
def open_tar_gz(io) # :nodoc:
-
Zlib::GzipReader.wrap io do |gzio|
-
tar = Gem::Package::TarReader.new gzio
-
-
yield tar
-
end
-
end
-
-
##
-
# Reads and loads checksums.yaml.gz from the tar file +gem+
-
-
1
def read_checksums(gem)
-
Gem.load_yaml
-
-
@checksums = gem.seek 'checksums.yaml.gz' do |entry|
-
Zlib::GzipReader.wrap entry do |gz_io|
-
Gem::SafeYAML.safe_load gz_io.read
-
end
-
end
-
end
-
-
##
-
# Prepares the gem for signing and checksum generation. If a signing
-
# certificate and key are not present only checksum generation is set up.
-
-
1
def setup_signer(signer_options: {})
-
passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE']
-
if @spec.signing_key
-
@signer =
-
Gem::Security::Signer.new(
-
@spec.signing_key,
-
@spec.cert_chain,
-
passphrase,
-
signer_options
-
)
-
-
@spec.signing_key = nil
-
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s }
-
else
-
@signer = Gem::Security::Signer.new nil, nil, passphrase
-
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if
-
@signer.cert_chain
-
end
-
end
-
-
##
-
# The spec for this gem.
-
#
-
# If this is a package for a built gem the spec is loaded from the
-
# gem and returned. If this is a package for a gem being built the provided
-
# spec is returned.
-
-
1
def spec
-
verify unless @spec
-
-
@spec
-
end
-
-
##
-
# Verifies that this gem:
-
#
-
# * Contains a valid gem specification
-
# * Contains a contents archive
-
# * The contents archive is not corrupt
-
#
-
# After verification the gem specification from the gem is available from
-
# #spec
-
-
1
def verify
-
@files = []
-
@spec = nil
-
-
@gem.with_read_io do |io|
-
Gem::Package::TarReader.new io do |reader|
-
read_checksums reader
-
-
verify_files reader
-
end
-
end
-
-
verify_checksums @digests, @checksums
-
-
@security_policy.verify_signatures @spec, @digests, @signatures if
-
@security_policy
-
-
true
-
rescue Gem::Security::Exception
-
@spec = nil
-
@files = []
-
raise
-
rescue Errno::ENOENT => e
-
raise Gem::Package::FormatError.new e.message
-
rescue Gem::Package::TarInvalidError => e
-
raise Gem::Package::FormatError.new e.message, @gem
-
end
-
-
##
-
# Verifies the +checksums+ against the +digests+. This check is not
-
# cryptographically secure. Missing checksums are ignored.
-
-
1
def verify_checksums(digests, checksums) # :nodoc:
-
return unless checksums
-
-
checksums.sort.each do |algorithm, gem_digests|
-
gem_digests.sort.each do |file_name, gem_hexdigest|
-
computed_digest = digests[algorithm][file_name]
-
-
unless computed_digest.hexdigest == gem_hexdigest
-
raise Gem::Package::FormatError.new \
-
"#{algorithm} checksum mismatch for #{file_name}", @gem
-
end
-
end
-
end
-
end
-
-
##
-
# Verifies +entry+ in a .gem file.
-
-
1
def verify_entry(entry)
-
file_name = entry.full_name
-
@files << file_name
-
-
case file_name
-
when /\.sig$/ then
-
@signatures[$`] = entry.read if @security_policy
-
return
-
else
-
digest entry
-
end
-
-
case file_name
-
when "metadata", "metadata.gz" then
-
load_spec entry
-
when 'data.tar.gz' then
-
verify_gz entry
-
end
-
rescue => e
-
message = "package is corrupt, exception while verifying: " +
-
"#{e.message} (#{e.class})"
-
raise Gem::Package::FormatError.new message, @gem
-
end
-
-
##
-
# Verifies the files of the +gem+
-
-
1
def verify_files(gem)
-
gem.each do |entry|
-
verify_entry entry
-
end
-
-
unless @spec
-
raise Gem::Package::FormatError.new 'package metadata is missing', @gem
-
end
-
-
unless @files.include? 'data.tar.gz'
-
raise Gem::Package::FormatError.new \
-
'package content (data.tar.gz) is missing', @gem
-
end
-
-
if duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first) and duplicates.any?
-
raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})"
-
end
-
end
-
-
##
-
# Verifies that +entry+ is a valid gzipped file.
-
-
1
def verify_gz(entry) # :nodoc:
-
Zlib::GzipReader.wrap entry do |gzio|
-
gzio.read 16384 until gzio.eof? # gzip checksum verification
-
end
-
rescue Zlib::GzipFile::Error => e
-
raise Gem::Package::FormatError.new(e.message, entry.full_name)
-
end
-
-
end
-
-
1
require 'rubygems/package/digest_io'
-
1
require 'rubygems/package/source'
-
1
require 'rubygems/package/file_source'
-
1
require 'rubygems/package/io_source'
-
1
require 'rubygems/package/old'
-
1
require 'rubygems/package/tar_header'
-
1
require 'rubygems/package/tar_reader'
-
1
require 'rubygems/package/tar_reader/entry'
-
1
require 'rubygems/package/tar_writer'
-
# frozen_string_literal: true
-
##
-
# IO wrapper that creates digests of contents written to the IO it wraps.
-
-
1
class Gem::Package::DigestIO
-
-
##
-
# Collected digests for wrapped writes.
-
#
-
# {
-
# 'SHA1' => #<OpenSSL::Digest: [...]>,
-
# 'SHA512' => #<OpenSSL::Digest: [...]>,
-
# }
-
-
1
attr_reader :digests
-
-
##
-
# Wraps +io+ and updates digest for each of the digest algorithms in
-
# the +digests+ Hash. Returns the digests hash. Example:
-
#
-
# io = StringIO.new
-
# digests = {
-
# 'SHA1' => OpenSSL::Digest.new('SHA1'),
-
# 'SHA512' => OpenSSL::Digest.new('SHA512'),
-
# }
-
#
-
# Gem::Package::DigestIO.wrap io, digests do |digest_io|
-
# digest_io.write "hello"
-
# end
-
#
-
# digests['SHA1'].hexdigest #=> "aaf4c61d[...]"
-
# digests['SHA512'].hexdigest #=> "9b71d224[...]"
-
-
1
def self.wrap(io, digests)
-
digest_io = new io, digests
-
-
yield digest_io
-
-
return digests
-
end
-
-
##
-
# Creates a new DigestIO instance. Using ::wrap is recommended, see the
-
# ::wrap documentation for documentation of +io+ and +digests+.
-
-
1
def initialize(io, digests)
-
@io = io
-
@digests = digests
-
end
-
-
##
-
# Writes +data+ to the underlying IO and updates the digests
-
-
1
def write(data)
-
result = @io.write data
-
-
@digests.each do |_, digest|
-
digest << data
-
end
-
-
result
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# The primary source of gems is a file on disk, including all usages
-
# internal to rubygems.
-
#
-
# This is a private class, do not depend on it directly. Instead, pass a path
-
# object to `Gem::Package.new`.
-
-
1
class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all
-
-
1
attr_reader :path
-
-
1
def initialize(path)
-
@path = path
-
end
-
-
1
def start
-
@start ||= File.read path, 20
-
end
-
-
1
def present?
-
File.exist? path
-
end
-
-
1
def with_write_io(&block)
-
File.open path, 'wb', &block
-
end
-
-
1
def with_read_io(&block)
-
File.open path, 'rb', &block
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# Supports reading and writing gems from/to a generic IO object. This is
-
# useful for other applications built on top of rubygems, such as
-
# rubygems.org.
-
#
-
# This is a private class, do not depend on it directly. Instead, pass an IO
-
# object to `Gem::Package.new`.
-
-
1
class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all
-
-
1
attr_reader :io
-
-
1
def initialize(io)
-
@io = io
-
end
-
-
1
def start
-
@start ||= begin
-
if io.pos > 0
-
raise Gem::Package::Error, "Cannot read start unless IO is at start"
-
end
-
-
value = io.read 20
-
io.rewind
-
value
-
end
-
end
-
-
1
def present?
-
true
-
end
-
-
1
def with_read_io
-
yield io
-
end
-
-
1
def with_write_io
-
yield io
-
end
-
-
1
def path
-
end
-
-
end
-
# frozen_string_literal: true
-
#--
-
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-
# All rights reserved.
-
# See LICENSE.txt for permissions.
-
#++
-
-
##
-
# The format class knows the guts of the ancient .gem file format and provides
-
# the capability to read such ancient gems.
-
#
-
# Please pretend this doesn't exist.
-
-
1
class Gem::Package::Old < Gem::Package
-
-
1
undef_method :spec=
-
-
##
-
# Creates a new old-format package reader for +gem+. Old-format packages
-
# cannot be written.
-
-
1
def initialize(gem, security_policy)
-
require 'fileutils'
-
require 'zlib'
-
Gem.load_yaml
-
-
@contents = nil
-
@gem = gem
-
@security_policy = security_policy
-
@spec = nil
-
end
-
-
##
-
# A list of file names contained in this gem
-
-
1
def contents
-
verify
-
-
return @contents if @contents
-
-
@gem.with_read_io do |io|
-
read_until_dashes io # spec
-
header = file_list io
-
-
@contents = header.map { |file| file['path'] }
-
end
-
end
-
-
##
-
# Extracts the files in this package into +destination_dir+
-
-
1
def extract_files(destination_dir)
-
verify
-
-
errstr = "Error reading files from gem"
-
-
@gem.with_read_io do |io|
-
read_until_dashes io # spec
-
header = file_list io
-
raise Gem::Exception, errstr unless header
-
-
header.each do |entry|
-
full_name = entry['path']
-
-
destination = install_location full_name, destination_dir
-
-
file_data = String.new
-
-
read_until_dashes io do |line|
-
file_data << line
-
end
-
-
file_data = file_data.strip.unpack("m")[0]
-
file_data = Zlib::Inflate.inflate file_data
-
-
raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if
-
file_data.length != entry['size'].to_i
-
-
FileUtils.rm_rf destination
-
-
FileUtils.mkdir_p File.dirname(destination), :mode => dir_mode && 0755
-
-
File.open destination, 'wb', file_mode(entry['mode']) do |out|
-
out.write file_data
-
end
-
-
verbose destination
-
end
-
end
-
rescue Zlib::DataError
-
raise Gem::Exception, errstr
-
end
-
-
##
-
# Reads the file list section from the old-format gem +io+
-
-
1
def file_list(io) # :nodoc:
-
header = String.new
-
-
read_until_dashes io do |line|
-
header << line
-
end
-
-
Gem::SafeYAML.safe_load header
-
end
-
-
##
-
# Reads lines until a "---" separator is found
-
-
1
def read_until_dashes(io) # :nodoc:
-
while (line = io.gets) && line.chomp.strip != "---" do
-
yield line if block_given?
-
end
-
end
-
-
##
-
# Skips the Ruby self-install header in +io+.
-
-
1
def skip_ruby(io) # :nodoc:
-
loop do
-
line = io.gets
-
-
return if line.chomp == '__END__'
-
break unless line
-
end
-
-
raise Gem::Exception, "Failed to find end of Ruby script while reading gem"
-
end
-
-
##
-
# The specification for this gem
-
-
1
def spec
-
verify
-
-
return @spec if @spec
-
-
yaml = String.new
-
-
@gem.with_read_io do |io|
-
skip_ruby io
-
read_until_dashes io do |line|
-
yaml << line
-
end
-
end
-
-
begin
-
@spec = Gem::Specification.from_yaml yaml
-
rescue YAML::SyntaxError
-
raise Gem::Exception, "Failed to parse gem specification out of gem file"
-
end
-
rescue ArgumentError
-
raise Gem::Exception, "Failed to parse gem specification out of gem file"
-
end
-
-
##
-
# Raises an exception if a security policy that verifies data is active.
-
# Old format gems cannot be verified as signed.
-
-
1
def verify
-
return true unless @security_policy
-
-
raise Gem::Security::Exception,
-
'old format gems do not contain signatures and cannot be verified' if
-
@security_policy.verify_data
-
-
true
-
end
-
-
end
-
# frozen_string_literal: true
-
1
class Gem::Package::Source # :nodoc:
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
-
##
-
#--
-
# struct tarfile_entry_posix {
-
# char name[100]; # ASCII + (Z unless filled)
-
# char mode[8]; # 0 padded, octal, null
-
# char uid[8]; # ditto
-
# char gid[8]; # ditto
-
# char size[12]; # 0 padded, octal, null
-
# char mtime[12]; # 0 padded, octal, null
-
# char checksum[8]; # 0 padded, octal, null, space
-
# char typeflag[1]; # file: "0" dir: "5"
-
# char linkname[100]; # ASCII + (Z unless filled)
-
# char magic[6]; # "ustar\0"
-
# char version[2]; # "00"
-
# char uname[32]; # ASCIIZ
-
# char gname[32]; # ASCIIZ
-
# char devmajor[8]; # 0 padded, octal, null
-
# char devminor[8]; # o padded, octal, null
-
# char prefix[155]; # ASCII + (Z unless filled)
-
# };
-
#++
-
# A header for a tar file
-
-
1
class Gem::Package::TarHeader
-
-
##
-
# Fields in the tar header
-
-
1
FIELDS = [
-
:checksum,
-
:devmajor,
-
:devminor,
-
:gid,
-
:gname,
-
:linkname,
-
:magic,
-
:mode,
-
:mtime,
-
:name,
-
:prefix,
-
:size,
-
:typeflag,
-
:uid,
-
:uname,
-
:version,
-
].freeze
-
-
##
-
# Pack format for a tar header
-
-
1
PACK_FORMAT = 'a100' + # name
-
'a8' + # mode
-
'a8' + # uid
-
'a8' + # gid
-
'a12' + # size
-
'a12' + # mtime
-
'a7a' + # chksum
-
'a' + # typeflag
-
'a100' + # linkname
-
'a6' + # magic
-
'a2' + # version
-
'a32' + # uname
-
'a32' + # gname
-
'a8' + # devmajor
-
'a8' + # devminor
-
'a155' # prefix
-
-
##
-
# Unpack format for a tar header
-
-
1
UNPACK_FORMAT = 'A100' + # name
-
'A8' + # mode
-
'A8' + # uid
-
'A8' + # gid
-
'A12' + # size
-
'A12' + # mtime
-
'A8' + # checksum
-
'A' + # typeflag
-
'A100' + # linkname
-
'A6' + # magic
-
'A2' + # version
-
'A32' + # uname
-
'A32' + # gname
-
'A8' + # devmajor
-
'A8' + # devminor
-
'A155' # prefix
-
-
1
attr_reader(*FIELDS)
-
-
1
EMPTY_HEADER = ("\0" * 512).freeze # :nodoc:
-
-
##
-
# Creates a tar header from IO +stream+
-
-
1
def self.from(stream)
-
671
header = stream.read 512
-
671
empty = (EMPTY_HEADER == header)
-
-
671
fields = header.unpack UNPACK_FORMAT
-
-
671
new :name => fields.shift,
-
:mode => strict_oct(fields.shift),
-
:uid => strict_oct(fields.shift),
-
:gid => strict_oct(fields.shift),
-
:size => strict_oct(fields.shift),
-
:mtime => strict_oct(fields.shift),
-
:checksum => strict_oct(fields.shift),
-
:typeflag => fields.shift,
-
:linkname => fields.shift,
-
:magic => fields.shift,
-
:version => strict_oct(fields.shift),
-
:uname => fields.shift,
-
:gname => fields.shift,
-
:devmajor => strict_oct(fields.shift),
-
:devminor => strict_oct(fields.shift),
-
:prefix => fields.shift,
-
-
:empty => empty
-
end
-
-
1
def self.strict_oct(str)
-
6039
return str.oct if str =~ /\A[0-7]*\z/
-
raise ArgumentError, "#{str.inspect} is not an octal string"
-
end
-
-
##
-
# Creates a new TarHeader using +vals+
-
-
1
def initialize(vals)
-
936
unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
-
raise ArgumentError, ":name, :size, :prefix and :mode required"
-
end
-
-
936
vals[:uid] ||= 0
-
936
vals[:gid] ||= 0
-
936
vals[:mtime] ||= 0
-
936
vals[:checksum] ||= ""
-
936
vals[:typeflag] = "0" if vals[:typeflag].nil? || vals[:typeflag].empty?
-
936
vals[:magic] ||= "ustar"
-
936
vals[:version] ||= "00"
-
936
vals[:uname] ||= "wheel"
-
936
vals[:gname] ||= "wheel"
-
936
vals[:devmajor] ||= 0
-
936
vals[:devminor] ||= 0
-
-
936
FIELDS.each do |name|
-
14976
instance_variable_set "@#{name}", vals[name]
-
end
-
-
936
@empty = vals[:empty]
-
end
-
-
##
-
# Is the tar entry empty?
-
-
1
def empty?
-
671
@empty
-
end
-
-
1
def ==(other) # :nodoc:
-
self.class === other and
-
@checksum == other.checksum and
-
@devmajor == other.devmajor and
-
@devminor == other.devminor and
-
@gid == other.gid and
-
@gname == other.gname and
-
@linkname == other.linkname and
-
@magic == other.magic and
-
@mode == other.mode and
-
@mtime == other.mtime and
-
@name == other.name and
-
@prefix == other.prefix and
-
@size == other.size and
-
@typeflag == other.typeflag and
-
@uid == other.uid and
-
@uname == other.uname and
-
@version == other.version
-
end
-
-
1
def to_s # :nodoc:
-
265
update_checksum
-
265
header
-
end
-
-
##
-
# Updates the TarHeader's checksum
-
-
1
def update_checksum
-
265
header = header " " * 8
-
265
@checksum = oct calculate_checksum(header), 6
-
end
-
-
1
private
-
-
1
def calculate_checksum(header)
-
135680
header.unpack("C*").inject { |a, b| a + b }
-
end
-
-
1
def header(checksum = @checksum)
-
header = [
-
530
name,
-
oct(mode, 7),
-
oct(uid, 7),
-
oct(gid, 7),
-
oct(size, 11),
-
oct(mtime, 11),
-
checksum,
-
" ",
-
typeflag,
-
linkname,
-
magic,
-
oct(version, 2),
-
uname,
-
gname,
-
oct(devmajor, 7),
-
oct(devminor, 7),
-
prefix
-
]
-
-
530
header = header.pack PACK_FORMAT
-
-
530
header << ("\0" * ((512 - header.size) % 512))
-
end
-
-
1
def oct(num, len)
-
4505
"%0#{len}o" % num
-
end
-
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#++
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#--
-
-
##
-
# Class for reading entries out of a tar file
-
-
1
class Gem::Package::TarReader::Entry
-
-
##
-
# Header for this tar entry
-
-
1
attr_reader :header
-
-
##
-
# Creates a new tar entry for +header+ that will be read from +io+
-
-
1
def initialize(header, io)
-
600
@closed = false
-
600
@header = header
-
600
@io = io
-
600
@orig_pos = @io.pos
-
600
@read = 0
-
end
-
-
1
def check_closed # :nodoc:
-
600
raise IOError, "closed #{self.class}" if closed?
-
end
-
-
##
-
# Number of bytes read out of the tar entry
-
-
1
def bytes_read
-
600
@read
-
end
-
-
##
-
# Closes the tar entry
-
-
1
def close
-
600
@closed = true
-
end
-
-
##
-
# Is the tar entry closed?
-
-
1
def closed?
-
600
@closed
-
end
-
-
##
-
# Are we at the end of the tar entry?
-
-
1
def eof?
-
check_closed
-
-
@read >= @header.size
-
end
-
-
##
-
# Full name of the tar entry
-
-
1
def full_name
-
600
if @header.prefix != ""
-
File.join @header.prefix, @header.name
-
else
-
600
@header.name
-
end
-
rescue ArgumentError => e
-
raise unless e.message == 'string contains null byte'
-
raise Gem::Package::TarInvalidError,
-
'tar is corrupt, name contains null byte'
-
end
-
-
##
-
# Read one byte from the tar entry
-
-
1
def getc
-
check_closed
-
-
return nil if @read >= @header.size
-
-
ret = @io.getc
-
@read += 1 if ret
-
-
ret
-
end
-
-
##
-
# Is this tar entry a directory?
-
-
1
def directory?
-
@header.typeflag == "5"
-
end
-
-
##
-
# Is this tar entry a file?
-
-
1
def file?
-
@header.typeflag == "0"
-
end
-
-
##
-
# Is this tar entry a symlink?
-
-
1
def symlink?
-
@header.typeflag == "2"
-
end
-
-
##
-
# The position in the tar entry
-
-
1
def pos
-
check_closed
-
-
bytes_read
-
end
-
-
1
def size
-
@header.size
-
end
-
-
1
alias length size
-
-
##
-
# Reads +len+ bytes from the tar file entry, or the rest of the entry if
-
# nil
-
-
1
def read(len = nil)
-
600
check_closed
-
-
600
return nil if @read >= @header.size
-
-
344
len ||= @header.size - @read
-
344
max_read = [len, @header.size - @read].min
-
-
344
ret = @io.read max_read
-
344
@read += ret.size
-
-
344
ret
-
end
-
-
1
def readpartial(maxlen = nil, outbuf = "".b)
-
check_closed
-
-
raise EOFError if @read >= @header.size
-
-
maxlen ||= @header.size - @read
-
max_read = [maxlen, @header.size - @read].min
-
-
@io.readpartial(max_read, outbuf)
-
@read += outbuf.size
-
-
outbuf
-
end
-
-
##
-
# Rewinds to the beginning of the tar file entry
-
-
1
def rewind
-
check_closed
-
-
@io.pos = @orig_pos
-
@read = 0
-
end
-
-
end
-
# -*- coding: utf-8 -*-
-
# frozen_string_literal: true
-
#--
-
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-
# See LICENSE.txt for additional licensing information.
-
#++
-
-
1
require 'digest'
-
-
##
-
# Allows writing of tar files
-
-
1
class Gem::Package::TarWriter
-
-
1
class FileOverflow < StandardError; end
-
-
##
-
# IO wrapper that allows writing a limited amount of data
-
-
1
class BoundedStream
-
-
##
-
# Maximum number of bytes that can be written
-
-
1
attr_reader :limit
-
-
##
-
# Number of bytes written
-
-
1
attr_reader :written
-
-
##
-
# Wraps +io+ and allows up to +limit+ bytes to be written
-
-
1
def initialize(io, limit)
-
265
@io = io
-
265
@limit = limit
-
265
@written = 0
-
end
-
-
##
-
# Writes +data+ onto the IO, raising a FileOverflow exception if the
-
# number of bytes will be more than #limit
-
-
1
def write(data)
-
265
if data.bytesize + @written > @limit
-
raise FileOverflow, "You tried to feed more data than fits in the file."
-
end
-
265
@io.write data
-
265
@written += data.bytesize
-
265
data.bytesize
-
end
-
-
end
-
-
##
-
# IO wrapper that provides only #write
-
-
1
class RestrictedStream
-
-
##
-
# Creates a new RestrictedStream wrapping +io+
-
-
1
def initialize(io)
-
@io = io
-
end
-
-
##
-
# Writes +data+ onto the IO
-
-
1
def write(data)
-
@io.write data
-
end
-
-
end
-
-
##
-
# Creates a new TarWriter, yielding it if a block is given
-
-
1
def self.new(io)
-
82
writer = super
-
-
82
return writer unless block_given?
-
-
begin
-
yield writer
-
ensure
-
writer.close
-
end
-
-
nil
-
end
-
-
##
-
# Creates a new TarWriter that will write to +io+
-
-
1
def initialize(io)
-
82
@io = io
-
82
@closed = false
-
end
-
-
##
-
# Adds file +name+ with permissions +mode+, and yields an IO for writing the
-
# file to
-
-
1
def add_file(name, mode) # :yields: io
-
check_closed
-
-
name, prefix = split_name name
-
-
init_pos = @io.pos
-
@io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header
-
-
yield RestrictedStream.new(@io) if block_given?
-
-
size = @io.pos - init_pos - 512
-
-
remainder = (512 - (size % 512)) % 512
-
@io.write "\0" * remainder
-
-
final_pos = @io.pos
-
@io.pos = init_pos
-
-
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
-
:size => size, :prefix => prefix,
-
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
-
-
@io.write header
-
@io.pos = final_pos
-
-
self
-
end
-
-
##
-
# Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
-
# the file. The +digest_algorithm+ is written to a read-only +name+.sum
-
# file following the given file contents containing the digest name and
-
# hexdigest separated by a tab.
-
#
-
# The created digest object is returned.
-
-
1
def add_file_digest(name, mode, digest_algorithms) # :yields: io
-
digests = digest_algorithms.map do |digest_algorithm|
-
digest = digest_algorithm.new
-
digest_name =
-
if digest.respond_to? :name
-
digest.name
-
else
-
/::([^:]+)$/ =~ digest_algorithm.name
-
$1
-
end
-
-
[digest_name, digest]
-
end
-
-
digests = Hash[*digests.flatten]
-
-
add_file name, mode do |io|
-
Gem::Package::DigestIO.wrap io, digests do |digest_io|
-
yield digest_io
-
end
-
end
-
-
digests
-
end
-
-
##
-
# Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
-
# the file. The +signer+ is used to add a digest file using its
-
# digest_algorithm per add_file_digest and a cryptographic signature in
-
# +name+.sig. If the signer has no key only the checksum file is added.
-
#
-
# Returns the digest.
-
-
1
def add_file_signed(name, mode, signer)
-
digest_algorithms = [
-
signer.digest_algorithm,
-
Digest::SHA512,
-
].compact.uniq
-
-
digests = add_file_digest name, mode, digest_algorithms do |io|
-
yield io
-
end
-
-
signature_digest = digests.values.compact.find do |digest|
-
digest_name =
-
if digest.respond_to? :name
-
digest.name
-
else
-
digest.class.name[/::([^:]+)\z/, 1]
-
end
-
-
digest_name == signer.digest_name
-
end
-
-
raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest
-
-
if signer.key
-
signature = signer.sign signature_digest.digest
-
-
add_file_simple "#{name}.sig", 0444, signature.length do |io|
-
io.write signature
-
end
-
end
-
-
digests
-
end
-
-
##
-
# Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO
-
# to write the file to.
-
-
1
def add_file_simple(name, mode, size) # :yields: io
-
265
check_closed
-
-
265
name, prefix = split_name name
-
-
265
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
-
:size => size, :prefix => prefix,
-
265
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now).to_s
-
-
265
@io.write header
-
265
os = BoundedStream.new @io, size
-
-
265
yield os if block_given?
-
-
265
min_padding = size - os.written
-
265
@io.write("\0" * min_padding)
-
-
265
remainder = (512 - (size % 512)) % 512
-
265
@io.write("\0" * remainder)
-
-
265
self
-
end
-
-
##
-
# Adds symlink +name+ with permissions +mode+, linking to +target+.
-
-
1
def add_symlink(name, target, mode)
-
check_closed
-
-
name, prefix = split_name name
-
-
header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
-
:size => 0, :typeflag => "2",
-
:linkname => target,
-
:prefix => prefix,
-
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now).to_s
-
-
@io.write header
-
-
self
-
end
-
-
##
-
# Raises IOError if the TarWriter is closed
-
-
1
def check_closed
-
265
raise IOError, "closed #{self.class}" if closed?
-
end
-
-
##
-
# Closes the TarWriter
-
-
1
def close
-
check_closed
-
-
@io.write "\0" * 1024
-
flush
-
-
@closed = true
-
end
-
-
##
-
# Is the TarWriter closed?
-
-
1
def closed?
-
265
@closed
-
end
-
-
##
-
# Flushes the TarWriter's IO
-
-
1
def flush
-
check_closed
-
-
@io.flush if @io.respond_to? :flush
-
end
-
-
##
-
# Creates a new directory in the tar file +name+ with +mode+
-
-
1
def mkdir(name, mode)
-
check_closed
-
-
name, prefix = split_name(name)
-
-
header = Gem::Package::TarHeader.new :name => name, :mode => mode,
-
:typeflag => "5", :size => 0,
-
:prefix => prefix,
-
:mtime => ENV["SOURCE_DATE_EPOCH"] ? Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc : Time.now
-
-
@io.write header
-
-
self
-
end
-
-
##
-
# Splits +name+ into a name and prefix that can fit in the TarHeader
-
-
1
def split_name(name) # :nodoc:
-
265
if name.bytesize > 256
-
raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)")
-
end
-
-
265
prefix = ''
-
265
if name.bytesize > 100
-
parts = name.split('/', -1) # parts are never empty here
-
name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/")
-
prefix = parts.join('/') # if empty, then it's impossible to split (parts is empty too)
-
while !parts.empty? && (prefix.bytesize > 155 || name.empty?)
-
name = parts.pop + '/' + name
-
prefix = parts.join('/')
-
end
-
-
if name.bytesize > 100 or prefix.empty?
-
raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)")
-
end
-
-
if prefix.bytesize > 155
-
raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)")
-
end
-
end
-
-
265
return name, prefix
-
end
-
-
end
-
# frozen_string_literal: true
-
#--
-
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-
# All rights reserved.
-
# See LICENSE.txt for permissions.
-
#++
-
-
1
require 'rubygems/exceptions'
-
1
require 'fileutils'
-
-
begin
-
1
require 'openssl'
-
rescue LoadError => e
-
raise unless (e.respond_to?(:path) && e.path == 'openssl') ||
-
e.message =~ / -- openssl$/
-
end
-
-
##
-
# = Signing gems
-
#
-
# The Gem::Security implements cryptographic signatures for gems. The section
-
# below is a step-by-step guide to using signed gems and generating your own.
-
#
-
# == Walkthrough
-
#
-
# === Building your certificate
-
#
-
# In order to start signing your gems, you'll need to build a private key and
-
# a self-signed certificate. Here's how:
-
#
-
# # build a private key and certificate for yourself:
-
# $ gem cert --build you@example.com
-
#
-
# This could take anywhere from a few seconds to a minute or two, depending on
-
# the speed of your computer (public key algorithms aren't exactly the
-
# speediest crypto algorithms in the world). When it's finished, you'll see
-
# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current
-
# directory.
-
#
-
# First things first: Move both files to ~/.gem if you don't already have a
-
# key and certificate in that directory. Ensure the file permissions make the
-
# key unreadable by others (by default the file is saved securely).
-
#
-
# Keep your private key hidden; if it's compromised, someone can sign packages
-
# as you (note: PKI has ways of mitigating the risk of stolen keys; more on
-
# that later).
-
#
-
# === Signing Gems
-
#
-
# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will
-
# automatically find your key and certificate in your home directory and use
-
# them to sign newly packaged gems.
-
#
-
# If your certificate is not self-signed (signed by a third party) RubyGems
-
# will attempt to load the certificate chain from the trusted certificates.
-
# Use <code>gem cert --add signing_cert.pem</code> to add your signers as
-
# trusted certificates. See below for further information on certificate
-
# chains.
-
#
-
# If you build your gem it will automatically be signed. If you peek inside
-
# your gem file, you'll see a couple of new files have been added:
-
#
-
# $ tar tf your-gem-1.0.gem
-
# metadata.gz
-
# metadata.gz.sum
-
# metadata.gz.sig # metadata signature
-
# data.tar.gz
-
# data.tar.gz.sum
-
# data.tar.gz.sig # data signature
-
#
-
# === Manually signing gems
-
#
-
# If you wish to store your key in a separate secure location you'll need to
-
# set your gems up for signing by hand. To do this, set the
-
# <code>signing_key</code> and <code>cert_chain</code> in the gemspec before
-
# packaging your gem:
-
#
-
# s.signing_key = '/secure/path/to/gem-private_key.pem'
-
# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem]
-
#
-
# When you package your gem with these options set RubyGems will automatically
-
# load your key and certificate from the secure paths.
-
#
-
# === Signed gems and security policies
-
#
-
# Now let's verify the signature. Go ahead and install the gem, but add the
-
# following options: <code>-P HighSecurity</code>, like this:
-
#
-
# # install the gem with using the security policy "HighSecurity"
-
# $ sudo gem install your.gem -P HighSecurity
-
#
-
# The <code>-P</code> option sets your security policy -- we'll talk about
-
# that in just a minute. Eh, what's this?
-
#
-
# $ gem install -P HighSecurity your-gem-1.0.gem
-
# ERROR: While executing gem ... (Gem::Security::Exception)
-
# root cert /CN=you/DC=example is not trusted
-
#
-
# The culprit here is the security policy. RubyGems has several different
-
# security policies. Let's take a short break and go over the security
-
# policies. Here's a list of the available security policies, and a brief
-
# description of each one:
-
#
-
# * NoSecurity - Well, no security at all. Signed packages are treated like
-
# unsigned packages.
-
# * LowSecurity - Pretty much no security. If a package is signed then
-
# RubyGems will make sure the signature matches the signing
-
# certificate, and that the signing certificate hasn't expired, but
-
# that's it. A malicious user could easily circumvent this kind of
-
# security.
-
# * MediumSecurity - Better than LowSecurity and NoSecurity, but still
-
# fallible. Package contents are verified against the signing
-
# certificate, and the signing certificate is checked for validity,
-
# and checked against the rest of the certificate chain (if you don't
-
# know what a certificate chain is, stay tuned, we'll get to that).
-
# The biggest improvement over LowSecurity is that MediumSecurity
-
# won't install packages that are signed by untrusted sources.
-
# Unfortunately, MediumSecurity still isn't totally secure -- a
-
# malicious user can still unpack the gem, strip the signatures, and
-
# distribute the gem unsigned.
-
# * HighSecurity - Here's the bugger that got us into this mess.
-
# The HighSecurity policy is identical to the MediumSecurity policy,
-
# except that it does not allow unsigned gems. A malicious user
-
# doesn't have a whole lot of options here; they can't modify the
-
# package contents without invalidating the signature, and they can't
-
# modify or remove signature or the signing certificate chain, or
-
# RubyGems will simply refuse to install the package. Oh well, maybe
-
# they'll have better luck causing problems for CPAN users instead :).
-
#
-
# The reason RubyGems refused to install your shiny new signed gem was because
-
# it was from an untrusted source. Well, your code is infallible (naturally),
-
# so you need to add yourself as a trusted source:
-
#
-
# # add trusted certificate
-
# gem cert --add ~/.gem/gem-public_cert.pem
-
#
-
# You've now added your public certificate as a trusted source. Now you can
-
# install packages signed by your private key without any hassle. Let's try
-
# the install command above again:
-
#
-
# # install the gem with using the HighSecurity policy (and this time
-
# # without any shenanigans)
-
# $ gem install -P HighSecurity your-gem-1.0.gem
-
# Successfully installed your-gem-1.0
-
# 1 gem installed
-
#
-
# This time RubyGems will accept your signed package and begin installing.
-
#
-
# While you're waiting for RubyGems to work it's magic, have a look at some of
-
# the other security commands by running <code>gem help cert</code>:
-
#
-
# Options:
-
# -a, --add CERT Add a trusted certificate.
-
# -l, --list [FILTER] List trusted certificates where the
-
# subject contains FILTER
-
# -r, --remove FILTER Remove trusted certificates where the
-
# subject contains FILTER
-
# -b, --build EMAIL_ADDR Build private key and self-signed
-
# certificate for EMAIL_ADDR
-
# -C, --certificate CERT Signing certificate for --sign
-
# -K, --private-key KEY Key for --sign or --build
-
# -s, --sign CERT Signs CERT with the key from -K
-
# and the certificate from -C
-
#
-
# We've already covered the <code>--build</code> option, and the
-
# <code>--add</code>, <code>--list</code>, and <code>--remove</code> commands
-
# seem fairly straightforward; they allow you to add, list, and remove the
-
# certificates in your trusted certificate list. But what's with this
-
# <code>--sign</code> option?
-
#
-
# === Certificate chains
-
#
-
# To answer that question, let's take a look at "certificate chains", a
-
# concept I mentioned earlier. There are a couple of problems with
-
# self-signed certificates: first of all, self-signed certificates don't offer
-
# a whole lot of security. Sure, the certificate says Yukihiro Matsumoto, but
-
# how do I know it was actually generated and signed by matz himself unless he
-
# gave me the certificate in person?
-
#
-
# The second problem is scalability. Sure, if there are 50 gem authors, then
-
# I have 50 trusted certificates, no problem. What if there are 500 gem
-
# authors? 1000? Having to constantly add new trusted certificates is a
-
# pain, and it actually makes the trust system less secure by encouraging
-
# RubyGems users to blindly trust new certificates.
-
#
-
# Here's where certificate chains come in. A certificate chain establishes an
-
# arbitrarily long chain of trust between an issuing certificate and a child
-
# certificate. So instead of trusting certificates on a per-developer basis,
-
# we use the PKI concept of certificate chains to build a logical hierarchy of
-
# trust. Here's a hypothetical example of a trust hierarchy based (roughly)
-
# on geography:
-
#
-
# --------------------------
-
# | rubygems@rubygems.org |
-
# --------------------------
-
# |
-
# -----------------------------------
-
# | |
-
# ---------------------------- -----------------------------
-
# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com |
-
# ---------------------------- -----------------------------
-
# | | | |
-
# --------------- ---------------- ----------- --------------
-
# | drbrain | | zenspider | | pabs@dc | | tomcope@dc |
-
# --------------- ---------------- ----------- --------------
-
#
-
#
-
# Now, rather than having 4 trusted certificates (one for drbrain, zenspider,
-
# pabs@dc, and tomecope@dc), a user could actually get by with one
-
# certificate, the "rubygems@rubygems.org" certificate.
-
#
-
# Here's how it works:
-
#
-
# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard
-
# of "drbrain", but his certificate has a valid signature from the
-
# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature
-
# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's
-
# much more reasonable for me to trust a package signed by "drbrain", because
-
# I can establish a chain to "rubygems@rubygems.org", which I do trust.
-
#
-
# === Signing certificates
-
#
-
# The <code>--sign</code> option allows all this to happen. A developer
-
# creates their build certificate with the <code>--build</code> option, then
-
# has their certificate signed by taking it with them to their next regional
-
# Ruby meetup (in our hypothetical example), and it's signed there by the
-
# person holding the regional RubyGems signing certificate, which is signed at
-
# the next RubyConf by the holder of the top-level RubyGems certificate. At
-
# each point the issuer runs the same command:
-
#
-
# # sign a certificate with the specified key and certificate
-
# # (note that this modifies client_cert.pem!)
-
# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem
-
# --sign client_cert.pem
-
#
-
# Then the holder of issued certificate (in this case, your buddy "drbrain"),
-
# can start using this signed certificate to sign RubyGems. By the way, in
-
# order to let everyone else know about his new fancy signed certificate,
-
# "drbrain" would save his newly signed certificate as
-
# <code>~/.gem/gem-public_cert.pem</code>
-
#
-
# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in
-
# the "real world", issuers actually generate the child certificate from a
-
# certificate request, rather than sign an existing certificate. And our
-
# hypothetical infrastructure is missing a certificate revocation system.
-
# These are that can be fixed in the future...
-
#
-
# At this point you should know how to do all of these new and interesting
-
# things:
-
#
-
# * build a gem signing key and certificate
-
# * adjust your security policy
-
# * modify your trusted certificate list
-
# * sign a certificate
-
#
-
# == Manually verifying signatures
-
#
-
# In case you don't trust RubyGems you can verify gem signatures manually:
-
#
-
# 1. Fetch and unpack the gem
-
#
-
# gem fetch some_signed_gem
-
# tar -xf some_signed_gem-1.0.gem
-
#
-
# 2. Grab the public key from the gemspec
-
#
-
# gem spec some_signed_gem-1.0.gem cert_chain | \
-
# ruby -ryaml -e 'puts YAML.load_documents($stdin)' > public_key.crt
-
#
-
# 3. Generate a SHA1 hash of the data.tar.gz
-
#
-
# openssl dgst -sha1 < data.tar.gz > my.hash
-
#
-
# 4. Verify the signature
-
#
-
# openssl rsautl -verify -inkey public_key.crt -certin \
-
# -in data.tar.gz.sig > verified.hash
-
#
-
# 5. Compare your hash to the verified hash
-
#
-
# diff -s verified.hash my.hash
-
#
-
# 6. Repeat 5 and 6 with metadata.gz
-
#
-
# == OpenSSL Reference
-
#
-
# The .pem files generated by --build and --sign are PEM files. Here's a
-
# couple of useful OpenSSL commands for manipulating them:
-
#
-
# # convert a PEM format X509 certificate into DER format:
-
# # (note: Windows .cer files are X509 certificates in DER format)
-
# $ openssl x509 -in input.pem -outform der -out output.der
-
#
-
# # print out the certificate in a human-readable format:
-
# $ openssl x509 -in input.pem -noout -text
-
#
-
# And you can do the same thing with the private key file as well:
-
#
-
# # convert a PEM format RSA key into DER format:
-
# $ openssl rsa -in input_key.pem -outform der -out output_key.der
-
#
-
# # print out the key in a human readable format:
-
# $ openssl rsa -in input_key.pem -noout -text
-
#
-
# == Bugs/TODO
-
#
-
# * There's no way to define a system-wide trust list.
-
# * custom security policies (from a YAML file, etc)
-
# * Simple method to generate a signed certificate request
-
# * Support for OCSP, SCVP, CRLs, or some other form of cert status check
-
# (list is in order of preference)
-
# * Support for encrypted private keys
-
# * Some sort of semi-formal trust hierarchy (see long-winded explanation
-
# above)
-
# * Path discovery (for gem certificate chains that don't have a self-signed
-
# root) -- by the way, since we don't have this, THE ROOT OF THE CERTIFICATE
-
# CHAIN MUST BE SELF SIGNED if Policy#verify_root is true (and it is for the
-
# MediumSecurity and HighSecurity policies)
-
# * Better explanation of X509 naming (ie, we don't have to use email
-
# addresses)
-
# * Honor AIA field (see note about OCSP above)
-
# * Honor extension restrictions
-
# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12
-
# file, instead of an array embedded in the metadata.
-
# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1.
-
#
-
# == Original author
-
#
-
# Paul Duncan <pabs@pablotron.org>
-
# http://pablotron.org/
-
-
1
module Gem::Security
-
-
##
-
# Gem::Security default exception type
-
-
1
class Exception < Gem::Exception; end
-
-
##
-
# Digest algorithm used to sign gems
-
-
DIGEST_ALGORITHM =
-
1
if defined?(OpenSSL::Digest::SHA256)
-
1
OpenSSL::Digest::SHA256
-
elsif defined?(OpenSSL::Digest::SHA1)
-
OpenSSL::Digest::SHA1
-
else
-
require 'digest'
-
Digest::SHA512
-
end
-
-
##
-
# Used internally to select the signing digest from all computed digests
-
-
DIGEST_NAME = # :nodoc:
-
1
if DIGEST_ALGORITHM.method_defined? :name
-
1
DIGEST_ALGORITHM.new.name
-
else
-
DIGEST_ALGORITHM.name[/::([^:]+)\z/, 1]
-
end
-
-
##
-
# Algorithm for creating the key pair used to sign gems
-
-
KEY_ALGORITHM =
-
1
if defined?(OpenSSL::PKey::RSA)
-
1
OpenSSL::PKey::RSA
-
end
-
-
##
-
# Length of keys created by KEY_ALGORITHM
-
-
1
KEY_LENGTH = 3072
-
-
##
-
# Cipher used to encrypt the key pair used to sign gems.
-
# Must be in the list returned by OpenSSL::Cipher.ciphers
-
-
1
KEY_CIPHER = OpenSSL::Cipher.new('AES-256-CBC') if defined?(OpenSSL::Cipher)
-
-
##
-
# One day in seconds
-
-
1
ONE_DAY = 86400
-
-
##
-
# One year in seconds
-
-
1
ONE_YEAR = ONE_DAY * 365
-
-
##
-
# The default set of extensions are:
-
#
-
# * The certificate is not a certificate authority
-
# * The key for the certificate may be used for key and data encipherment
-
# and digital signatures
-
# * The certificate contains a subject key identifier
-
-
EXTENSIONS = {
-
1
'basicConstraints' => 'CA:FALSE',
-
'keyUsage' =>
-
'keyEncipherment,dataEncipherment,digitalSignature',
-
'subjectKeyIdentifier' => 'hash',
-
}.freeze
-
-
1
def self.alt_name_or_x509_entry(certificate, x509_entry)
-
alt_name = certificate.extensions.find do |extension|
-
extension.oid == "#{x509_entry}AltName"
-
end
-
-
return alt_name.value if alt_name
-
-
certificate.send x509_entry
-
end
-
-
##
-
# Creates an unsigned certificate for +subject+ and +key+. The lifetime of
-
# the key is from the current time to +age+ which defaults to one year.
-
#
-
# The +extensions+ restrict the key to the indicated uses.
-
-
1
def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS,
-
serial = 1)
-
cert = OpenSSL::X509::Certificate.new
-
-
cert.public_key = key.public_key
-
cert.version = 2
-
cert.serial = serial
-
-
cert.not_before = Time.now
-
cert.not_after = Time.now + age
-
-
cert.subject = subject
-
-
ef = OpenSSL::X509::ExtensionFactory.new nil, cert
-
-
cert.extensions = extensions.map do |ext_name, value|
-
ef.create_extension ext_name, value
-
end
-
-
cert
-
end
-
-
##
-
# Creates a self-signed certificate with an issuer and subject from +email+,
-
# a subject alternative name of +email+ and the given +extensions+ for the
-
# +key+.
-
-
1
def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS)
-
subject = email_to_name email
-
-
extensions = extensions.merge "subjectAltName" => "email:#{email}"
-
-
create_cert_self_signed subject, key, age, extensions
-
end
-
-
##
-
# Creates a self-signed certificate with an issuer and subject of +subject+
-
# and the given +extensions+ for the +key+.
-
-
1
def self.create_cert_self_signed(subject, key, age = ONE_YEAR,
-
extensions = EXTENSIONS, serial = 1)
-
certificate = create_cert subject, key, age, extensions
-
-
sign certificate, key, certificate, age, extensions, serial
-
end
-
-
##
-
# Creates a new key pair of the specified +length+ and +algorithm+. The
-
# default is a 3072 bit RSA key.
-
-
1
def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM)
-
algorithm.new length
-
end
-
-
##
-
# Turns +email_address+ into an OpenSSL::X509::Name
-
-
1
def self.email_to_name(email_address)
-
email_address = email_address.gsub(/[^\w@.-]+/i, '_')
-
-
cn, dcs = email_address.split '@'
-
-
dcs = dcs.split '.'
-
-
name = "CN=#{cn}/#{dcs.map { |dc| "DC=#{dc}" }.join '/'}"
-
-
OpenSSL::X509::Name.parse name
-
end
-
-
##
-
# Signs +expired_certificate+ with +private_key+ if the keys match and the
-
# expired certificate was self-signed.
-
#--
-
# TODO increment serial
-
-
1
def self.re_sign(expired_certificate, private_key, age = ONE_YEAR,
-
extensions = EXTENSIONS)
-
raise Gem::Security::Exception,
-
"incorrect signing key for re-signing " +
-
"#{expired_certificate.subject}" unless
-
expired_certificate.public_key.to_pem == private_key.public_key.to_pem
-
-
unless expired_certificate.subject.to_s ==
-
expired_certificate.issuer.to_s
-
subject = alt_name_or_x509_entry expired_certificate, :subject
-
issuer = alt_name_or_x509_entry expired_certificate, :issuer
-
-
raise Gem::Security::Exception,
-
"#{subject} is not self-signed, contact #{issuer} " +
-
"to obtain a valid certificate"
-
end
-
-
serial = expired_certificate.serial + 1
-
-
create_cert_self_signed(expired_certificate.subject, private_key, age,
-
extensions, serial)
-
end
-
-
##
-
# Resets the trust directory for verifying gems.
-
-
1
def self.reset
-
1
@trust_dir = nil
-
end
-
-
##
-
# Sign the public key from +certificate+ with the +signing_key+ and
-
# +signing_cert+, using the Gem::Security::DIGEST_ALGORITHM. Uses the
-
# default certificate validity range and extensions.
-
#
-
# Returns the newly signed certificate.
-
-
1
def self.sign(certificate, signing_key, signing_cert,
-
age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
-
signee_subject = certificate.subject
-
signee_key = certificate.public_key
-
-
alt_name = certificate.extensions.find do |extension|
-
extension.oid == 'subjectAltName'
-
end
-
-
extensions = extensions.merge 'subjectAltName' => alt_name.value if
-
alt_name
-
-
issuer_alt_name = signing_cert.extensions.find do |extension|
-
extension.oid == 'subjectAltName'
-
end
-
-
extensions = extensions.merge 'issuerAltName' => issuer_alt_name.value if
-
issuer_alt_name
-
-
signed = create_cert signee_subject, signee_key, age, extensions, serial
-
signed.issuer = signing_cert.subject
-
-
signed.sign signing_key, Gem::Security::DIGEST_ALGORITHM.new
-
end
-
-
##
-
# Returns a Gem::Security::TrustDir which wraps the directory where trusted
-
# certificates live.
-
-
1
def self.trust_dir
-
return @trust_dir if @trust_dir
-
-
dir = File.join Gem.user_home, '.gem', 'trust'
-
-
@trust_dir ||= Gem::Security::TrustDir.new dir
-
end
-
-
##
-
# Enumerates the trusted certificates via Gem::Security::TrustDir.
-
-
1
def self.trusted_certificates(&block)
-
trust_dir.each_certificate(&block)
-
end
-
-
##
-
# Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given
-
# +permissions+. If passed +cipher+ and +passphrase+ those arguments will be
-
# passed to +to_pem+.
-
-
1
def self.write(pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER)
-
path = File.expand_path path
-
-
File.open path, 'wb', permissions do |io|
-
if passphrase and cipher
-
io.write pemmable.to_pem cipher, passphrase
-
else
-
io.write pemmable.to_pem
-
end
-
end
-
-
path
-
end
-
-
1
reset
-
-
end
-
-
1
if defined?(OpenSSL::SSL)
-
1
require 'rubygems/security/policy'
-
1
require 'rubygems/security/policies'
-
1
require 'rubygems/security/trust_dir'
-
end
-
-
1
require 'rubygems/security/signer'
-
# frozen_string_literal: true
-
1
module Gem::Security
-
-
##
-
# No security policy: all package signature checks are disabled.
-
-
1
NoSecurity = Policy.new(
-
'No Security',
-
:verify_data => false,
-
:verify_signer => false,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# AlmostNo security policy: only verify that the signing certificate is the
-
# one that actually signed the data. Make no attempt to verify the signing
-
# certificate chain.
-
#
-
# This policy is basically useless. better than nothing, but can still be
-
# easily spoofed, and is not recommended.
-
-
1
AlmostNoSecurity = Policy.new(
-
'Almost No Security',
-
:verify_data => true,
-
:verify_signer => false,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Low security policy: only verify that the signing certificate is actually
-
# the gem signer, and that the signing certificate is valid.
-
#
-
# This policy is better than nothing, but can still be easily spoofed, and
-
# is not recommended.
-
-
1
LowSecurity = Policy.new(
-
'Low Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => false,
-
:verify_root => false,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Medium security policy: verify the signing certificate, verify the signing
-
# certificate chain all the way to the root certificate, and only trust root
-
# certificates that we have explicitly allowed trust for.
-
#
-
# This security policy is reasonable, but it allows unsigned packages, so a
-
# malicious person could simply delete the package signature and pass the
-
# gem off as unsigned.
-
-
1
MediumSecurity = Policy.new(
-
'Medium Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => true,
-
:only_signed => false
-
)
-
-
##
-
# High security policy: only allow signed gems to be installed, verify the
-
# signing certificate, verify the signing certificate chain all the way to
-
# the root certificate, and only trust root certificates that we have
-
# explicitly allowed trust for.
-
#
-
# This security policy is significantly more difficult to bypass, and offers
-
# a reasonable guarantee that the contents of the gem have not been altered.
-
-
1
HighSecurity = Policy.new(
-
'High Security',
-
:verify_data => true,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => true,
-
:only_signed => true
-
)
-
-
##
-
# Policy used to verify a certificate and key when signing a gem
-
-
1
SigningPolicy = Policy.new(
-
'Signing Policy',
-
:verify_data => false,
-
:verify_signer => true,
-
:verify_chain => true,
-
:verify_root => true,
-
:only_trusted => false,
-
:only_signed => false
-
)
-
-
##
-
# Hash of configured security policies
-
-
Policies = {
-
1
'NoSecurity' => NoSecurity,
-
'AlmostNoSecurity' => AlmostNoSecurity,
-
'LowSecurity' => LowSecurity,
-
'MediumSecurity' => MediumSecurity,
-
'HighSecurity' => HighSecurity,
-
# SigningPolicy is not intended for use by `gem -P` so do not list it
-
}.freeze
-
-
end
-
# frozen_string_literal: true
-
1
require 'rubygems/user_interaction'
-
-
##
-
# A Gem::Security::Policy object encapsulates the settings for verifying
-
# signed gem files. This is the base class. You can either declare an
-
# instance of this or use one of the preset security policies in
-
# Gem::Security::Policies.
-
-
1
class Gem::Security::Policy
-
-
1
include Gem::UserInteraction
-
-
1
attr_reader :name
-
-
1
attr_accessor :only_signed
-
1
attr_accessor :only_trusted
-
1
attr_accessor :verify_chain
-
1
attr_accessor :verify_data
-
1
attr_accessor :verify_root
-
1
attr_accessor :verify_signer
-
-
##
-
# Create a new Gem::Security::Policy object with the given mode and
-
# options.
-
-
1
def initialize(name, policy = {}, opt = {})
-
6
require 'openssl'
-
-
6
@name = name
-
-
6
@opt = opt
-
-
# Default to security
-
6
@only_signed = true
-
6
@only_trusted = true
-
6
@verify_chain = true
-
6
@verify_data = true
-
6
@verify_root = true
-
6
@verify_signer = true
-
-
6
policy.each_pair do |key, val|
-
36
case key
-
6
when :verify_data then @verify_data = val
-
6
when :verify_signer then @verify_signer = val
-
6
when :verify_chain then @verify_chain = val
-
6
when :verify_root then @verify_root = val
-
6
when :only_trusted then @only_trusted = val
-
6
when :only_signed then @only_signed = val
-
end
-
end
-
end
-
-
##
-
# Verifies each certificate in +chain+ has signed the following certificate
-
# and is valid for the given +time+.
-
-
1
def check_chain(chain, time)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
raise Gem::Security::Exception, 'empty signing chain' if chain.empty?
-
-
begin
-
chain.each_cons 2 do |issuer, cert|
-
check_cert cert, issuer, time
-
end
-
-
true
-
rescue Gem::Security::Exception => e
-
raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
-
end
-
end
-
-
##
-
# Verifies that +data+ matches the +signature+ created by +public_key+ and
-
# the +digest+ algorithm.
-
-
1
def check_data(public_key, digest, signature, data)
-
raise Gem::Security::Exception, "invalid signature" unless
-
public_key.verify digest.new, signature, data.digest
-
-
true
-
end
-
-
##
-
# Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
-
# If the +issuer+ is +nil+ no verification is performed.
-
-
1
def check_cert(signer, issuer, time)
-
raise Gem::Security::Exception, 'missing signing certificate' unless
-
signer
-
-
message = "certificate #{signer.subject}"
-
-
if not_before = signer.not_before and not_before > time
-
raise Gem::Security::Exception,
-
"#{message} not valid before #{not_before}"
-
end
-
-
if not_after = signer.not_after and not_after < time
-
raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
-
end
-
-
if issuer and not signer.verify issuer.public_key
-
raise Gem::Security::Exception,
-
"#{message} was not issued by #{issuer.subject}"
-
end
-
-
true
-
end
-
-
##
-
# Ensures the public key of +key+ matches the public key in +signer+
-
-
1
def check_key(signer, key)
-
unless signer and key
-
return true unless @only_signed
-
-
raise Gem::Security::Exception, 'missing key or signature'
-
end
-
-
raise Gem::Security::Exception,
-
"certificate #{signer.subject} does not match the signing key" unless
-
signer.public_key.to_pem == key.public_key.to_pem
-
-
true
-
end
-
-
##
-
# Ensures the root certificate in +chain+ is self-signed and valid for
-
# +time+.
-
-
1
def check_root(chain, time)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
-
root = chain.first
-
-
raise Gem::Security::Exception, 'missing root certificate' unless root
-
-
raise Gem::Security::Exception,
-
"root certificate #{root.subject} is not self-signed " +
-
"(issuer #{root.issuer})" if
-
root.issuer.to_s != root.subject.to_s # HACK to_s is for ruby 1.8
-
-
check_cert root, root, time
-
end
-
-
##
-
# Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and
-
# the digests of the two certificates match according to +digester+
-
-
1
def check_trust(chain, digester, trust_dir)
-
raise Gem::Security::Exception, 'missing signing chain' unless chain
-
-
root = chain.first
-
-
raise Gem::Security::Exception, 'missing root certificate' unless root
-
-
path = Gem::Security.trust_dir.cert_path root
-
-
unless File.exist? path
-
message = "root cert #{root.subject} is not trusted".dup
-
-
message << " (root of signing cert #{chain.last.subject})" if
-
chain.length > 1
-
-
raise Gem::Security::Exception, message
-
end
-
-
save_cert = OpenSSL::X509::Certificate.new File.read path
-
save_dgst = digester.digest save_cert.public_key.to_s
-
-
pkey_str = root.public_key.to_s
-
cert_dgst = digester.digest pkey_str
-
-
raise Gem::Security::Exception,
-
"trusted root certificate #{root.subject} checksum " +
-
"does not match signing root certificate checksum" unless
-
save_dgst == cert_dgst
-
-
true
-
end
-
-
##
-
# Extracts the email or subject from +certificate+
-
-
1
def subject(certificate) # :nodoc:
-
certificate.extensions.each do |extension|
-
next unless extension.oid == 'subjectAltName'
-
-
return extension.value
-
end
-
-
certificate.subject.to_s
-
end
-
-
1
def inspect # :nodoc:
-
("[Policy: %s - data: %p signer: %p chain: %p root: %p " +
-
"signed-only: %p trusted-only: %p]") % [
-
@name, @verify_chain, @verify_data, @verify_root, @verify_signer,
-
@only_signed, @only_trusted,
-
]
-
end
-
-
##
-
# For +full_name+, verifies the certificate +chain+ is valid, the +digests+
-
# match the signatures +signatures+ created by the signer depending on the
-
# +policy+ settings.
-
#
-
# If +key+ is given it is used to validate the signing certificate.
-
-
1
def verify(chain, key = nil, digests = {}, signatures = {},
-
full_name = '(unknown)')
-
if signatures.empty?
-
if @only_signed
-
raise Gem::Security::Exception,
-
"unsigned gems are not allowed by the #{name} policy"
-
elsif digests.empty?
-
# lack of signatures is irrelevant if there is nothing to check
-
# against
-
else
-
alert_warning "#{full_name} is not signed"
-
return
-
end
-
end
-
-
opt = @opt
-
digester = Gem::Security::DIGEST_ALGORITHM
-
trust_dir = opt[:trust_dir]
-
time = Time.now
-
-
_, signer_digests = digests.find do |algorithm, file_digests|
-
file_digests.values.first.name == Gem::Security::DIGEST_NAME
-
end
-
-
if @verify_data
-
raise Gem::Security::Exception, 'no digests provided (probable bug)' if
-
signer_digests.nil? or signer_digests.empty?
-
else
-
signer_digests = {}
-
end
-
-
signer = chain.last
-
-
check_key signer, key if key
-
-
check_cert signer, nil, time if @verify_signer
-
-
check_chain chain, time if @verify_chain
-
-
check_root chain, time if @verify_root
-
-
if @only_trusted
-
check_trust chain, digester, trust_dir
-
elsif signatures.empty? and digests.empty?
-
# trust is irrelevant if there's no signatures to verify
-
else
-
alert_warning "#{subject signer} is not trusted for #{full_name}"
-
end
-
-
signatures.each do |file, _|
-
digest = signer_digests[file]
-
-
raise Gem::Security::Exception, "missing digest for #{file}" unless
-
digest
-
end
-
-
signer_digests.each do |file, digest|
-
signature = signatures[file]
-
-
raise Gem::Security::Exception, "missing signature for #{file}" unless
-
signature
-
-
check_data signer.public_key, digester, signature, digest if @verify_data
-
end
-
-
true
-
end
-
-
##
-
# Extracts the certificate chain from the +spec+ and calls #verify to ensure
-
# the signatures and certificate chain is valid according to the policy..
-
-
1
def verify_signatures(spec, digests, signatures)
-
chain = spec.cert_chain.map do |cert_pem|
-
OpenSSL::X509::Certificate.new cert_pem
-
end
-
-
verify chain, nil, digests, signatures, spec.full_name
-
-
true
-
end
-
-
1
alias to_s name # :nodoc:
-
-
end
-
# frozen_string_literal: true
-
##
-
# Basic OpenSSL-based package signing class.
-
-
1
require "rubygems/user_interaction"
-
-
1
class Gem::Security::Signer
-
-
1
include Gem::UserInteraction
-
-
##
-
# The chain of certificates for signing including the signing certificate
-
-
1
attr_accessor :cert_chain
-
-
##
-
# The private key for the signing certificate
-
-
1
attr_accessor :key
-
-
##
-
# The digest algorithm used to create the signature
-
-
1
attr_reader :digest_algorithm
-
-
##
-
# The name of the digest algorithm, used to pull digests out of the hash by
-
# name.
-
-
1
attr_reader :digest_name # :nodoc:
-
-
##
-
# Gem::Security::Signer options
-
-
1
attr_reader :options
-
-
DEFAULT_OPTIONS = {
-
1
expiration_length_days: 365
-
}.freeze
-
-
##
-
# Attemps to re-sign an expired cert with a given private key
-
1
def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
-
return unless expired_cert.not_after < Time.now
-
-
expiry = expired_cert.not_after.strftime('%Y%m%d%H%M%S')
-
expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
-
new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
-
-
Gem::Security.write(expired_cert, new_expired_cert_path)
-
-
re_signed_cert = Gem::Security.re_sign(
-
expired_cert,
-
private_key,
-
(Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days)
-
)
-
-
Gem::Security.write(re_signed_cert, expired_cert_path)
-
-
yield(expired_cert_path, new_expired_cert_path) if block_given?
-
end
-
-
##
-
# Creates a new signer with an RSA +key+ or path to a key, and a certificate
-
# +chain+ containing X509 certificates, encoding certificates or paths to
-
# certificates.
-
-
1
def initialize(key, cert_chain, passphrase = nil, options = {})
-
@cert_chain = cert_chain
-
@key = key
-
@passphrase = passphrase
-
@options = DEFAULT_OPTIONS.merge(options)
-
-
unless @key
-
default_key = File.join Gem.default_key_path
-
@key = default_key if File.exist? default_key
-
end
-
-
unless @cert_chain
-
default_cert = File.join Gem.default_cert_path
-
@cert_chain = [default_cert] if File.exist? default_cert
-
end
-
-
@digest_algorithm = Gem::Security::DIGEST_ALGORITHM
-
@digest_name = Gem::Security::DIGEST_NAME
-
-
if @key && !@key.is_a?(OpenSSL::PKey::RSA)
-
@passphrase ||= ask_for_password("Enter PEM pass phrase:")
-
@key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase)
-
end
-
-
if @cert_chain
-
@cert_chain = @cert_chain.compact.map do |cert|
-
next cert if OpenSSL::X509::Certificate === cert
-
-
cert = File.read cert if File.exist? cert
-
-
OpenSSL::X509::Certificate.new cert
-
end
-
-
load_cert_chain
-
end
-
end
-
-
##
-
# Extracts the full name of +cert+. If the certificate has a subjectAltName
-
# this value is preferred, otherwise the subject is used.
-
-
1
def extract_name(cert) # :nodoc:
-
subject_alt_name = cert.extensions.find { |e| 'subjectAltName' == e.oid }
-
-
if subject_alt_name
-
/\Aemail:/ =~ subject_alt_name.value
-
-
$' || subject_alt_name.value
-
else
-
cert.subject
-
end
-
end
-
-
##
-
# Loads any missing issuers in the cert chain from the trusted certificates.
-
#
-
# If the issuer does not exist it is ignored as it will be checked later.
-
-
1
def load_cert_chain # :nodoc:
-
return if @cert_chain.empty?
-
-
while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do
-
issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first
-
-
break unless issuer # cert chain is verified later
-
-
@cert_chain.unshift issuer
-
end
-
end
-
-
##
-
# Sign data with given digest algorithm
-
-
1
def sign(data)
-
return unless @key
-
-
raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty?
-
-
if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now
-
re_sign_key(
-
expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
-
)
-
end
-
-
full_name = extract_name @cert_chain.last
-
-
Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
-
-
@key.sign @digest_algorithm.new, data
-
end
-
-
##
-
# Attempts to re-sign the private key if the signing certificate is expired.
-
#
-
# The key will be re-signed if:
-
# * The expired certificate is self-signed
-
# * The expired certificate is saved at ~/.gem/gem-public_cert.pem
-
# and the private key is saved at ~/.gem/gem-private_key.pem
-
# * There is no file matching the expiry date at
-
# ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S
-
#
-
# If the signing certificate can be re-signed the expired certificate will
-
# be saved as ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S where the
-
# expiry time (not after) is used for the timestamp.
-
-
1
def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc:
-
old_cert = @cert_chain.last
-
-
disk_cert_path = File.join(Gem.default_cert_path)
-
disk_cert = File.read(disk_cert_path) rescue nil
-
-
disk_key_path = File.join(Gem.default_key_path)
-
disk_key =
-
OpenSSL::PKey::RSA.new(File.read(disk_key_path), @passphrase) rescue nil
-
-
return unless disk_key
-
-
if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem
-
expiry = old_cert.not_after.strftime('%Y%m%d%H%M%S')
-
old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
-
old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file)
-
-
unless File.exist?(old_cert_path)
-
Gem::Security.write(old_cert, old_cert_path)
-
-
cert = Gem::Security.re_sign(old_cert, @key, expiration_length)
-
-
Gem::Security.write(cert, disk_cert_path)
-
-
alert("Your cert: #{disk_cert_path} has been auto re-signed with the key: #{disk_key_path}")
-
alert("Your expired cert will be located at: #{old_cert_path}")
-
-
@cert_chain = [cert]
-
end
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
##
-
# The TrustDir manages the trusted certificates for gem signature
-
# verification.
-
-
1
class Gem::Security::TrustDir
-
-
##
-
# Default permissions for the trust directory and its contents
-
-
DEFAULT_PERMISSIONS = {
-
1
:trust_dir => 0700,
-
:trusted_cert => 0600,
-
}.freeze
-
-
##
-
# The directory where trusted certificates will be stored.
-
-
1
attr_reader :dir
-
-
##
-
# Creates a new TrustDir using +dir+ where the directory and file
-
# permissions will be checked according to +permissions+
-
-
1
def initialize(dir, permissions = DEFAULT_PERMISSIONS)
-
@dir = dir
-
@permissions = permissions
-
-
@digester = Gem::Security::DIGEST_ALGORITHM
-
end
-
-
##
-
# Returns the path to the trusted +certificate+
-
-
1
def cert_path(certificate)
-
name_path certificate.subject
-
end
-
-
##
-
# Enumerates trusted certificates.
-
-
1
def each_certificate
-
return enum_for __method__ unless block_given?
-
-
glob = File.join @dir, '*.pem'
-
-
Dir[glob].each do |certificate_file|
-
begin
-
certificate = load_certificate certificate_file
-
-
yield certificate, certificate_file
-
rescue OpenSSL::X509::CertificateError
-
next # HACK warn
-
end
-
end
-
end
-
-
##
-
# Returns the issuer certificate of the given +certificate+ if it exists in
-
# the trust directory.
-
-
1
def issuer_of(certificate)
-
path = name_path certificate.issuer
-
-
return unless File.exist? path
-
-
load_certificate path
-
end
-
-
##
-
# Returns the path to the trusted certificate with the given ASN.1 +name+
-
-
1
def name_path(name)
-
digest = @digester.hexdigest name.to_s
-
-
File.join @dir, "cert-#{digest}.pem"
-
end
-
-
##
-
# Loads the given +certificate_file+
-
-
1
def load_certificate(certificate_file)
-
pem = File.read certificate_file
-
-
OpenSSL::X509::Certificate.new pem
-
end
-
-
##
-
# Add a certificate to trusted certificate list.
-
-
1
def trust_cert(certificate)
-
verify
-
-
destination = cert_path certificate
-
-
File.open destination, 'wb', 0600 do |io|
-
io.write certificate.to_pem
-
io.chmod(@permissions[:trusted_cert])
-
end
-
end
-
-
##
-
# Make sure the trust directory exists. If it does exist, make sure it's
-
# actually a directory. If not, then create it with the appropriate
-
# permissions.
-
-
1
def verify
-
if File.exist? @dir
-
raise Gem::Security::Exception,
-
"trust directory #{@dir} is not a directory" unless
-
File.directory? @dir
-
-
FileUtils.chmod 0700, @dir
-
else
-
FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir]
-
end
-
end
-
-
end
-
# frozen_string_literal: true
-
1
require 'rubygems'
-
-
##
-
# A collection of text-wrangling methods
-
-
1
module Gem::Text
-
-
##
-
# Remove any non-printable characters and make the text suitable for
-
# printing.
-
1
def clean_text(text)
-
text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".".freeze)
-
end
-
-
1
def truncate_text(text, description, max_length = 100_000)
-
raise ArgumentError, "max_length must be positive" unless max_length > 0
-
return text if text.size <= max_length
-
"Truncating #{description} to #{max_length.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse} characters:\n" + text[0, max_length]
-
end
-
-
##
-
# Wraps +text+ to +wrap+ characters and optionally indents by +indent+
-
# characters
-
-
1
def format_text(text, wrap, indent=0)
-
result = []
-
work = clean_text(text)
-
-
while work.length > wrap do
-
if work =~ /^(.{0,#{wrap}})[ \n]/
-
result << $1.rstrip
-
work.slice!(0, $&.length)
-
else
-
result << work.slice!(0, wrap)
-
end
-
end
-
-
result << work if work.length.nonzero?
-
result.join("\n").gsub(/^/, " " * indent)
-
end
-
-
1
def min3(a, b, c) # :nodoc:
-
if a < b && a < c
-
a
-
elsif b < c
-
b
-
else
-
c
-
end
-
end
-
-
# This code is based directly on the Text gem implementation
-
# Returns a value representing the "cost" of transforming str1 into str2
-
1
def levenshtein_distance(str1, str2)
-
s = str1
-
t = str2
-
n = s.length
-
m = t.length
-
-
return m if (0 == n)
-
return n if (0 == m)
-
-
d = (0..m).to_a
-
x = nil
-
-
str1.each_char.each_with_index do |char1,i|
-
e = i+1
-
-
str2.each_char.each_with_index do |char2,j|
-
cost = (char1 == char2) ? 0 : 1
-
x = min3(
-
d[j+1] + 1, # insertion
-
e + 1, # deletion
-
d[j] + cost # substitution
-
)
-
d[j] = e
-
e = x
-
end
-
-
d[m] = x
-
end
-
-
return x
-
end
-
end
-
# frozen_string_literal: true
-
#--
-
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-
# All rights reserved.
-
# See LICENSE.txt for permissions.
-
#++
-
-
1
require 'rubygems/util'
-
1
require 'rubygems/deprecate'
-
1
require 'rubygems/text'
-
-
##
-
# Module that defines the default UserInteraction. Any class including this
-
# module will have access to the +ui+ method that returns the default UI.
-
-
1
module Gem::DefaultUserInteraction
-
-
1
include Gem::Text
-
-
##
-
# The default UI is a class variable of the singleton class for this
-
# module.
-
-
1
@ui = nil
-
-
##
-
# Return the default UI.
-
-
1
def self.ui
-
@ui ||= Gem::ConsoleUI.new
-
end
-
-
##
-
# Set the default UI. If the default UI is never explicitly set, a simple
-
# console based UserInteraction will be used automatically.
-
-
1
def self.ui=(new_ui)
-
@ui = new_ui
-
end
-
-
##
-
# Use +new_ui+ for the duration of +block+.
-
-
1
def self.use_ui(new_ui)
-
old_ui = @ui
-
@ui = new_ui
-
yield
-
ensure
-
@ui = old_ui
-
end
-
-
##
-
# See DefaultUserInteraction::ui
-
-
1
def ui
-
Gem::DefaultUserInteraction.ui
-
end
-
-
##
-
# See DefaultUserInteraction::ui=
-
-
1
def ui=(new_ui)
-
Gem::DefaultUserInteraction.ui = new_ui
-
end
-
-
##
-
# See DefaultUserInteraction::use_ui
-
-
1
def use_ui(new_ui, &block)
-
Gem::DefaultUserInteraction.use_ui(new_ui, &block)
-
end
-
-
end
-
-
##
-
# UserInteraction allows RubyGems to interact with the user through standard
-
# methods that can be replaced with more-specific UI methods for different
-
# displays.
-
#
-
# Since UserInteraction dispatches to a concrete UI class you may need to
-
# reference other classes for specific behavior such as Gem::ConsoleUI or
-
# Gem::SilentUI.
-
#
-
# Example:
-
#
-
# class X
-
# include Gem::UserInteraction
-
#
-
# def get_answer
-
# n = ask("What is the meaning of life?")
-
# end
-
# end
-
-
1
module Gem::UserInteraction
-
-
1
include Gem::DefaultUserInteraction
-
-
##
-
# Displays an alert +statement+. Asks a +question+ if given.
-
-
1
def alert(statement, question = nil)
-
ui.alert statement, question
-
end
-
-
##
-
# Displays an error +statement+ to the error output location. Asks a
-
# +question+ if given.
-
-
1
def alert_error(statement, question = nil)
-
ui.alert_error statement, question
-
end
-
-
##
-
# Displays a warning +statement+ to the warning output location. Asks a
-
# +question+ if given.
-
-
1
def alert_warning(statement, question = nil)
-
ui.alert_warning statement, question
-
end
-
-
##
-
# Asks a +question+ and returns the answer.
-
-
1
def ask(question)
-
ui.ask question
-
end
-
-
##
-
# Asks for a password with a +prompt+
-
-
1
def ask_for_password(prompt)
-
ui.ask_for_password prompt
-
end
-
-
##
-
# Asks a yes or no +question+. Returns true for yes, false for no.
-
-
1
def ask_yes_no(question, default = nil)
-
ui.ask_yes_no question, default
-
end
-
-
##
-
# Asks the user to answer +question+ with an answer from the given +list+.
-
-
1
def choose_from_list(question, list)
-
ui.choose_from_list question, list
-
end
-
-
##
-
# Displays the given +statement+ on the standard output (or equivalent).
-
-
1
def say(statement = '')
-
ui.say statement
-
end
-
-
##
-
# Terminates the RubyGems process with the given +exit_code+
-
-
1
def terminate_interaction(exit_code = 0)
-
ui.terminate_interaction exit_code
-
end
-
-
##
-
# Calls +say+ with +msg+ or the results of the block if really_verbose
-
# is true.
-
-
1
def verbose(msg = nil)
-
say(clean_text(msg || yield)) if Gem.configuration.really_verbose
-
end
-
end
-
-
##
-
# Gem::StreamUI implements a simple stream based user interface.
-
-
1
class Gem::StreamUI
-
-
1
extend Gem::Deprecate
-
-
##
-
# The input stream
-
-
1
attr_reader :ins
-
-
##
-
# The output stream
-
-
1
attr_reader :outs
-
-
##
-
# The error stream
-
-
1
attr_reader :errs
-
-
##
-
# Creates a new StreamUI wrapping +in_stream+ for user input, +out_stream+
-
# for standard output, +err_stream+ for error output. If +usetty+ is true
-
# then special operations (like asking for passwords) will use the TTY
-
# commands to disable character echo.
-
-
1
def initialize(in_stream, out_stream, err_stream=STDERR, usetty=true)
-
@ins = in_stream
-
@outs = out_stream
-
@errs = err_stream
-
@usetty = usetty
-
end
-
-
##
-
# Returns true if TTY methods should be used on this StreamUI.
-
-
1
def tty?
-
@usetty && @ins.tty?
-
end
-
-
##
-
# Prints a formatted backtrace to the errors stream if backtraces are
-
# enabled.
-
-
1
def backtrace(exception)
-
return unless Gem.configuration.backtrace
-
-
@errs.puts "\t#{exception.backtrace.join "\n\t"}"
-
end
-
-
##
-
# Choose from a list of options. +question+ is a prompt displayed above
-
# the list. +list+ is a list of option strings. Returns the pair
-
# [option_name, option_index].
-
-
1
def choose_from_list(question, list)
-
@outs.puts question
-
-
list.each_with_index do |item, index|
-
@outs.puts " #{index+1}. #{item}"
-
end
-
-
@outs.print "> "
-
@outs.flush
-
-
result = @ins.gets
-
-
return nil, nil unless result
-
-
result = result.strip.to_i - 1
-
return list[result], result
-
end
-
-
##
-
# Ask a question. Returns a true for yes, false for no. If not connected
-
# to a tty, raises an exception if default is nil, otherwise returns
-
# default.
-
-
1
def ask_yes_no(question, default=nil)
-
unless tty?
-
if default.nil?
-
raise Gem::OperationNotSupportedError,
-
"Not connected to a tty and no default specified"
-
else
-
return default
-
end
-
end
-
-
default_answer = case default
-
when nil
-
'yn'
-
when true
-
'Yn'
-
else
-
'yN'
-
end
-
-
result = nil
-
-
while result.nil? do
-
result = case ask "#{question} [#{default_answer}]"
-
when /^y/i then true
-
when /^n/i then false
-
when /^$/ then default
-
else nil
-
end
-
end
-
-
return result
-
end
-
-
##
-
# Ask a question. Returns an answer if connected to a tty, nil otherwise.
-
-
1
def ask(question)
-
return nil if not tty?
-
-
@outs.print(question + " ")
-
@outs.flush
-
-
result = @ins.gets
-
result.chomp! if result
-
result
-
end
-
-
##
-
# Ask for a password. Does not echo response to terminal.
-
-
1
def ask_for_password(question)
-
return nil if not tty?
-
-
@outs.print(question, " ")
-
@outs.flush
-
-
password = _gets_noecho
-
@outs.puts
-
password.chomp! if password
-
password
-
end
-
-
1
def require_io_console
-
@require_io_console ||= begin
-
begin
-
require 'io/console'
-
rescue LoadError
-
end
-
true
-
end
-
end
-
-
1
def _gets_noecho
-
require_io_console
-
@ins.noecho {@ins.gets}
-
end
-
-
##
-
# Display a statement.
-
-
1
def say(statement="")
-
@outs.puts statement
-
end
-
-
##
-
# Display an informational alert. Will ask +question+ if it is not nil.
-
-
1
def alert(statement, question=nil)
-
@outs.puts "INFO: #{statement}"
-
ask(question) if question
-
end
-
-
##
-
# Display a warning on stderr. Will ask +question+ if it is not nil.
-
-
1
def alert_warning(statement, question=nil)
-
@errs.puts "WARNING: #{statement}"
-
ask(question) if question
-
end
-
-
##
-
# Display an error message in a location expected to get error messages.
-
# Will ask +question+ if it is not nil.
-
-
1
def alert_error(statement, question=nil)
-
@errs.puts "ERROR: #{statement}"
-
ask(question) if question
-
end
-
-
##
-
# Display a debug message on the same location as error messages.
-
-
1
def debug(statement)
-
@errs.puts statement
-
end
-
1
deprecate :debug, :none, 2018, 12
-
-
##
-
# Terminate the application with exit code +status+, running any exit
-
# handlers that might have been defined.
-
-
1
def terminate_interaction(status = 0)
-
close
-
raise Gem::SystemExitException, status
-
end
-
-
1
def close
-
end
-
-
##
-
# Return a progress reporter object chosen from the current verbosity.
-
-
1
def progress_reporter(*args)
-
case Gem.configuration.verbose
-
when nil, false
-
SilentProgressReporter.new(@outs, *args)
-
when true
-
SimpleProgressReporter.new(@outs, *args)
-
else
-
VerboseProgressReporter.new(@outs, *args)
-
end
-
end
-
-
##
-
# An absolutely silent progress reporter.
-
-
1
class SilentProgressReporter
-
-
##
-
# The count of items is never updated for the silent progress reporter.
-
-
1
attr_reader :count
-
-
##
-
# Creates a silent progress reporter that ignores all input arguments.
-
-
1
def initialize(out_stream, size, initial_message, terminal_message = nil)
-
end
-
-
##
-
# Does not print +message+ when updated as this object has taken a vow of
-
# silence.
-
-
1
def updated(message)
-
end
-
-
##
-
# Does not print anything when complete as this object has taken a vow of
-
# silence.
-
-
1
def done
-
end
-
end
-
-
##
-
# A basic dotted progress reporter.
-
-
1
class SimpleProgressReporter
-
-
1
include Gem::DefaultUserInteraction
-
-
##
-
# The number of progress items counted so far.
-
-
1
attr_reader :count
-
-
##
-
# Creates a new progress reporter that will write to +out_stream+ for
-
# +size+ items. Shows the given +initial_message+ when progress starts
-
# and the +terminal_message+ when it is complete.
-
-
1
def initialize(out_stream, size, initial_message,
-
terminal_message = "complete")
-
@out = out_stream
-
@total = size
-
@count = 0
-
@terminal_message = terminal_message
-
-
@out.puts initial_message
-
end
-
-
##
-
# Prints out a dot and ignores +message+.
-
-
1
def updated(message)
-
@count += 1
-
@out.print "."
-
@out.flush
-
end
-
-
##
-
# Prints out the terminal message.
-
-
1
def done
-
@out.puts "\n#{@terminal_message}"
-
end
-
-
end
-
-
##
-
# A progress reporter that prints out messages about the current progress.
-
-
1
class VerboseProgressReporter
-
-
1
include Gem::DefaultUserInteraction
-
-
##
-
# The number of progress items counted so far.
-
-
1
attr_reader :count
-
-
##
-
# Creates a new progress reporter that will write to +out_stream+ for
-
# +size+ items. Shows the given +initial_message+ when progress starts
-
# and the +terminal_message+ when it is complete.
-
-
1
def initialize(out_stream, size, initial_message,
-
terminal_message = 'complete')
-
@out = out_stream
-
@total = size
-
@count = 0
-
@terminal_message = terminal_message
-
-
@out.puts initial_message
-
end
-
-
##
-
# Prints out the position relative to the total and the +message+.
-
-
1
def updated(message)
-
@count += 1
-
@out.puts "#{@count}/#{@total}: #{message}"
-
end
-
-
##
-
# Prints out the terminal message.
-
-
1
def done
-
@out.puts @terminal_message
-
end
-
end
-
-
##
-
# Return a download reporter object chosen from the current verbosity
-
-
1
def download_reporter(*args)
-
if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty?
-
SilentDownloadReporter.new(@outs, *args)
-
else
-
ThreadedDownloadReporter.new(@outs, *args)
-
end
-
end
-
-
##
-
# An absolutely silent download reporter.
-
-
1
class SilentDownloadReporter
-
-
##
-
# The silent download reporter ignores all arguments
-
-
1
def initialize(out_stream, *args)
-
end
-
-
##
-
# The silent download reporter does not display +filename+ or care about
-
# +filesize+ because it is silent.
-
-
1
def fetch(filename, filesize)
-
end
-
-
##
-
# Nothing can update the silent download reporter.
-
-
1
def update(current)
-
end
-
-
##
-
# The silent download reporter won't tell you when the download is done.
-
# Because it is silent.
-
-
1
def done
-
end
-
end
-
-
##
-
# A progress reporter that behaves nicely with threaded downloading.
-
-
1
class ThreadedDownloadReporter
-
-
1
MUTEX = Mutex.new
-
-
##
-
# The current file name being displayed
-
-
1
attr_reader :file_name
-
-
##
-
# Creates a new threaded download reporter that will display on
-
# +out_stream+. The other arguments are ignored.
-
-
1
def initialize(out_stream, *args)
-
@file_name = nil
-
@out = out_stream
-
end
-
-
##
-
# Tells the download reporter that the +file_name+ is being fetched.
-
# The other arguments are ignored.
-
-
1
def fetch(file_name, *args)
-
if @file_name.nil?
-
@file_name = file_name
-
locked_puts "Fetching #{@file_name}"
-
end
-
end
-
-
##
-
# Updates the threaded download reporter for the given number of +bytes+.
-
-
1
def update(bytes)
-
# Do nothing.
-
end
-
-
##
-
# Indicates the download is complete.
-
-
1
def done
-
# Do nothing.
-
end
-
-
1
private
-
1
def locked_puts(message)
-
MUTEX.synchronize do
-
@out.puts message
-
end
-
end
-
end
-
end
-
-
##
-
# Subclass of StreamUI that instantiates the user interaction using STDIN,
-
# STDOUT, and STDERR.
-
-
1
class Gem::ConsoleUI < Gem::StreamUI
-
-
##
-
# The Console UI has no arguments as it defaults to reading input from
-
# stdin, output to stdout and warnings or errors to stderr.
-
-
1
def initialize
-
super STDIN, STDOUT, STDERR, true
-
end
-
end
-
-
##
-
# SilentUI is a UI choice that is absolutely silent.
-
-
1
class Gem::SilentUI < Gem::StreamUI
-
-
##
-
# The SilentUI has no arguments as it does not use any stream.
-
-
1
def initialize
-
reader, writer = nil, nil
-
-
reader = File.open(IO::NULL, 'r')
-
writer = File.open(IO::NULL, 'w')
-
-
super reader, writer, writer, false
-
end
-
-
1
def close
-
super
-
@ins.close
-
@outs.close
-
end
-
-
1
def download_reporter(*args) # :nodoc:
-
SilentDownloadReporter.new(@outs, *args)
-
end
-
-
1
def progress_reporter(*args) # :nodoc:
-
SilentProgressReporter.new(@outs, *args)
-
end
-
end
-
# -*- coding: us-ascii -*-
-
# frozen_string_literal: true
-
-
# == Secure random number generator interface.
-
#
-
# This library is an interface to secure random number generators which are
-
# suitable for generating session keys in HTTP cookies, etc.
-
#
-
# You can use this library in your application by requiring it:
-
#
-
# require 'securerandom'
-
#
-
# It supports the following secure random number generators:
-
#
-
# * openssl
-
# * /dev/urandom
-
# * Win32
-
#
-
# === Examples
-
#
-
# Generate random hexadecimal strings:
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.hex(10) #=> "52750b30ffbc7de3b362"
-
# SecureRandom.hex(10) #=> "92b15d6c8dc4beb5f559"
-
# SecureRandom.hex(13) #=> "39b290146bea6ce975c37cfc23"
-
#
-
# Generate random base64 strings:
-
#
-
# SecureRandom.base64(10) #=> "EcmTPZwWRAozdA=="
-
# SecureRandom.base64(10) #=> "KO1nIU+p9DKxGg=="
-
# SecureRandom.base64(12) #=> "7kJSM/MzBJI+75j8"
-
#
-
# Generate random binary strings:
-
#
-
# SecureRandom.random_bytes(10) #=> "\016\t{\370g\310pbr\301"
-
# SecureRandom.random_bytes(10) #=> "\323U\030TO\234\357\020\a\337"
-
#
-
# Generate alphanumeric strings:
-
#
-
# SecureRandom.alphanumeric(10) #=> "S8baxMJnPl"
-
# SecureRandom.alphanumeric(10) #=> "aOxAg8BAJe"
-
#
-
# Generate UUIDs:
-
#
-
# SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
-
# SecureRandom.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
-
#
-
-
1
module SecureRandom
-
1
@rng_chooser = Mutex.new # :nodoc:
-
-
1
class << self
-
1
def bytes(n)
-
return gen_random(n)
-
end
-
-
1
def gen_random(n)
-
1
ret = Random.urandom(1)
-
1
if ret.nil?
-
begin
-
require 'openssl'
-
rescue NoMethodError
-
raise NotImplementedError, "No random device"
-
else
-
@rng_chooser.synchronize do
-
class << self
-
remove_method :gen_random
-
alias gen_random gen_random_openssl
-
public :gen_random
-
end
-
end
-
return gen_random(n)
-
end
-
else
-
1
@rng_chooser.synchronize do
-
1
class << self
-
1
remove_method :gen_random
-
1
alias gen_random gen_random_urandom
-
1
public :gen_random
-
end
-
end
-
1
return gen_random(n)
-
end
-
end
-
-
1
private
-
-
1
def gen_random_openssl(n)
-
@pid = 0 unless defined?(@pid)
-
pid = $$
-
unless @pid == pid
-
now = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
-
OpenSSL::Random.random_add([now, @pid, pid].join(""), 0.0)
-
seed = Random.urandom(16)
-
if (seed)
-
OpenSSL::Random.random_add(seed, 16)
-
end
-
@pid = pid
-
end
-
return OpenSSL::Random.random_bytes(n)
-
end
-
-
1
def gen_random_urandom(n)
-
79
ret = Random.urandom(n)
-
79
unless ret
-
raise NotImplementedError, "No random device"
-
end
-
79
unless ret.length == n
-
raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
-
end
-
79
ret
-
end
-
end
-
end
-
-
1
module Random::Formatter
-
-
# SecureRandom.random_bytes generates a random binary string.
-
#
-
# The argument _n_ specifies the length of the result string.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in future.
-
#
-
# The result may contain any byte: "\x00" - "\xff".
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.random_bytes #=> "\xD8\\\xE0\xF4\r\xB2\xFC*WM\xFF\x83\x18\xF45\xB6"
-
# SecureRandom.random_bytes #=> "m\xDC\xFC/\a\x00Uf\xB2\xB2P\xBD\xFF6S\x97"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def random_bytes(n=nil)
-
79
n = n ? n.to_int : 16
-
79
gen_random(n)
-
end
-
-
# SecureRandom.hex generates a random hexadecimal string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number to be generated.
-
# The length of the resulting hexadecimal string is twice of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain 0-9 and a-f.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.hex #=> "eb693ec8252cd630102fd0d0fb7c3485"
-
# SecureRandom.hex #=> "91dc3bfb4de5b11d029d376634589b61"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def hex(n=nil)
-
random_bytes(n).unpack("H*")[0]
-
end
-
-
# SecureRandom.base64 generates a random base64 string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number
-
# to be generated. The length of the result string is about 4/3 of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain A-Z, a-z, 0-9, "+", "/" and "=".
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.base64 #=> "/2BuBuLf3+WfSKyQbRcc/A=="
-
# SecureRandom.base64 #=> "6BbW0pxO0YENxn38HMUbcQ=="
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
#
-
# See RFC 3548 for the definition of base64.
-
1
def base64(n=nil)
-
[random_bytes(n)].pack("m0")
-
end
-
-
# SecureRandom.urlsafe_base64 generates a random URL-safe base64 string.
-
#
-
# The argument _n_ specifies the length, in bytes, of the random number
-
# to be generated. The length of the result string is about 4/3 of _n_.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The boolean argument _padding_ specifies the padding.
-
# If it is false or nil, padding is not generated.
-
# Otherwise padding is generated.
-
# By default, padding is not generated because "=" may be used as a URL delimiter.
-
#
-
# The result may contain A-Z, a-z, 0-9, "-" and "_".
-
# "=" is also used if _padding_ is true.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
-
# SecureRandom.urlsafe_base64 #=> "UZLdOkzop70Ddx-IJR0ABg"
-
#
-
# SecureRandom.urlsafe_base64(nil, true) #=> "i0XQ-7gglIsHGV2_BNPrdQ=="
-
# SecureRandom.urlsafe_base64(nil, true) #=> "-M8rLhr7JEpJlqFGUMmOxg=="
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
#
-
# See RFC 3548 for the definition of URL-safe base64.
-
1
def urlsafe_base64(n=nil, padding=false)
-
79
s = [random_bytes(n)].pack("m0")
-
79
s.tr!("+/", "-_")
-
79
s.delete!("=") unless padding
-
79
s
-
end
-
-
# SecureRandom.uuid generates a random v4 UUID (Universally Unique IDentifier).
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.uuid #=> "2d931510-d99f-494a-8c67-87feb05e1594"
-
# SecureRandom.uuid #=> "bad85eb9-0713-4da7-8d36-07a8e4b00eab"
-
# SecureRandom.uuid #=> "62936e70-1815-439b-bf89-8492855a7e6b"
-
#
-
# The version 4 UUID is purely random (except the version).
-
# It doesn't contain meaningful information such as MAC addresses, timestamps, etc.
-
#
-
# The result contains 122 random bits (15.25 random bytes).
-
#
-
# See RFC 4122 for details of UUID.
-
#
-
1
def uuid
-
ary = random_bytes(16).unpack("NnnnnN")
-
ary[2] = (ary[2] & 0x0fff) | 0x4000
-
ary[3] = (ary[3] & 0x3fff) | 0x8000
-
"%08x-%04x-%04x-%04x-%04x%08x" % ary
-
end
-
-
1
private def gen_random(n)
-
self.bytes(n)
-
end
-
-
# SecureRandom.choose generates a string that randomly draws from a
-
# source array of characters.
-
#
-
# The argument _source_ specifies the array of characters from which
-
# to generate the string.
-
# The argument _n_ specifies the length, in characters, of the string to be
-
# generated.
-
#
-
# The result may contain whatever characters are in the source array.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.choose([*'l'..'r'], 16) #=> "lmrqpoonmmlqlron"
-
# SecureRandom.choose([*'0'..'9'], 5) #=> "27309"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
private def choose(source, n)
-
size = source.size
-
m = 1
-
limit = size
-
while limit * size <= 0x100000000
-
limit *= size
-
m += 1
-
end
-
result = ''.dup
-
while m <= n
-
rs = random_number(limit)
-
is = rs.digits(size)
-
(m-is.length).times { is << 0 }
-
result << source.values_at(*is).join('')
-
n -= m
-
end
-
if 0 < n
-
rs = random_number(limit)
-
is = rs.digits(size)
-
if is.length < n
-
(n-is.length).times { is << 0 }
-
else
-
is.pop while n < is.length
-
end
-
result.concat source.values_at(*is).join('')
-
end
-
result
-
end
-
-
1
ALPHANUMERIC = [*'A'..'Z', *'a'..'z', *'0'..'9']
-
# SecureRandom.alphanumeric generates a random alphanumeric string.
-
#
-
# The argument _n_ specifies the length, in characters, of the alphanumeric
-
# string to be generated.
-
#
-
# If _n_ is not specified or is nil, 16 is assumed.
-
# It may be larger in the future.
-
#
-
# The result may contain A-Z, a-z and 0-9.
-
#
-
# require 'securerandom'
-
#
-
# SecureRandom.alphanumeric #=> "2BuBuLf3WfSKyQbR"
-
# SecureRandom.alphanumeric(10) #=> "i6K93NdqiH"
-
#
-
# If a secure random number generator is not available,
-
# +NotImplementedError+ is raised.
-
1
def alphanumeric(n=nil)
-
n = 16 if n.nil?
-
choose(ALPHANUMERIC, n)
-
end
-
end
-
-
1
SecureRandom.extend(Random::Formatter)
-
# frozen_string_literal: true
-
-
1
require 'socket.so'
-
1
require 'io/wait'
-
-
1
class Addrinfo
-
# creates an Addrinfo object from the arguments.
-
#
-
# The arguments are interpreted as similar to self.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).family_addrinfo("www.ruby-lang.org", 80)
-
# #=> #<Addrinfo: 221.186.184.68:80 TCP (www.ruby-lang.org:80)>
-
#
-
# Addrinfo.unix("/tmp/sock").family_addrinfo("/tmp/sock2")
-
# #=> #<Addrinfo: /tmp/sock2 SOCK_STREAM>
-
#
-
1
def family_addrinfo(*args)
-
if args.empty?
-
raise ArgumentError, "no address specified"
-
elsif Addrinfo === args.first
-
raise ArgumentError, "too many arguments" if args.length != 1
-
addrinfo = args.first
-
if (self.pfamily != addrinfo.pfamily) ||
-
(self.socktype != addrinfo.socktype)
-
raise ArgumentError, "Addrinfo type mismatch"
-
end
-
addrinfo
-
elsif self.ip?
-
raise ArgumentError, "IP address needs host and port but #{args.length} arguments given" if args.length != 2
-
host, port = args
-
Addrinfo.getaddrinfo(host, port, self.pfamily, self.socktype, self.protocol)[0]
-
elsif self.unix?
-
raise ArgumentError, "UNIX socket needs single path argument but #{args.length} arguments given" if args.length != 1
-
path, = args
-
Addrinfo.unix(path)
-
else
-
raise ArgumentError, "unexpected family"
-
end
-
end
-
-
# creates a new Socket connected to the address of +local_addrinfo+.
-
#
-
# If _local_addrinfo_ is nil, the address of the socket is not bound.
-
#
-
# The _timeout_ specify the seconds for timeout.
-
# Errno::ETIMEDOUT is raised when timeout occur.
-
#
-
# If a block is given the created socket is yielded for each address.
-
#
-
1
def connect_internal(local_addrinfo, timeout=nil) # :yields: socket
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.bind local_addrinfo if local_addrinfo
-
if timeout
-
case sock.connect_nonblock(self, exception: false)
-
when 0 # success or EISCONN, other errors raise
-
break
-
when :wait_writable
-
sock.wait_writable(timeout) or
-
raise Errno::ETIMEDOUT, 'user specified timeout'
-
end while true
-
else
-
sock.connect(self)
-
end
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
1
protected :connect_internal
-
-
# :call-seq:
-
# addrinfo.connect_from([local_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_from([local_addr_args], [opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# If one or more arguments given as _local_addr_args_,
-
# it is used as the local address of the socket.
-
# _local_addr_args_ is given for family_addrinfo to obtain actual address.
-
#
-
# If _local_addr_args_ is not given, the local address of the socket is not bound.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from("0.0.0.0", 4649) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
# # Addrinfo object can be taken for the argument.
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect_from(Addrinfo.tcp("0.0.0.0", 4649)) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_from(*args, timeout: nil, &block)
-
connect_internal(family_addrinfo(*args), timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect([opts]) {|socket| ... }
-
# addrinfo.connect([opts])
-
#
-
# creates a socket connected to the address of self.
-
#
-
# The optional argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("www.ruby-lang.org", 80).connect {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect(timeout: nil, &block)
-
connect_internal(nil, timeout, &block)
-
end
-
-
# :call-seq:
-
# addrinfo.connect_to([remote_addr_args], [opts]) {|socket| ... }
-
# addrinfo.connect_to([remote_addr_args], [opts])
-
#
-
# creates a socket connected to _remote_addr_args_ and bound to self.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:timeout] specify the timeout in seconds.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.tcp("0.0.0.0", 4649).connect_to("www.ruby-lang.org", 80) {|s|
-
# s.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# puts s.read
-
# }
-
#
-
1
def connect_to(*args, timeout: nil, &block)
-
remote_addrinfo = family_addrinfo(*args)
-
remote_addrinfo.connect_internal(self, timeout, &block)
-
end
-
-
# creates a socket bound to self.
-
#
-
# If a block is given, it is called with the socket and the value of the block is returned.
-
# The socket is returned otherwise.
-
#
-
# Addrinfo.udp("0.0.0.0", 9981).bind {|s|
-
# s.local_address.connect {|s| s.send "hello", 0 }
-
# p s.recv(10) #=> "hello"
-
# }
-
#
-
1
def bind
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a listening socket bound to self.
-
1
def listen(backlog=Socket::SOMAXCONN)
-
sock = Socket.new(self.pfamily, self.socktype, self.protocol)
-
begin
-
sock.ipv6only! if self.ipv6?
-
sock.setsockopt(:SOCKET, :REUSEADDR, 1)
-
sock.bind(self)
-
sock.listen(backlog)
-
rescue Exception
-
sock.close
-
raise
-
end
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# iterates over the list of Addrinfo objects obtained by Addrinfo.getaddrinfo.
-
#
-
# Addrinfo.foreach(nil, 80) {|x| p x }
-
# #=> #<Addrinfo: 127.0.0.1:80 TCP (:80)>
-
# # #<Addrinfo: 127.0.0.1:80 UDP (:80)>
-
# # #<Addrinfo: [::1]:80 TCP (:80)>
-
# # #<Addrinfo: [::1]:80 UDP (:80)>
-
#
-
1
def self.foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, &block)
-
Addrinfo.getaddrinfo(nodename, service, family, socktype, protocol, flags).each(&block)
-
end
-
end
-
-
1
class BasicSocket < IO
-
# Returns an address of the socket suitable for connect in the local machine.
-
#
-
# This method returns _self_.local_address, except following condition.
-
#
-
# - IPv4 unspecified address (0.0.0.0) is replaced by IPv4 loopback address (127.0.0.1).
-
# - IPv6 unspecified address (::) is replaced by IPv6 loopback address (::1).
-
#
-
# If the local address is not suitable for connect, SocketError is raised.
-
# IPv4 and IPv6 address which port is 0 is not suitable for connect.
-
# Unix domain socket which has no path is not suitable for connect.
-
#
-
# Addrinfo.tcp("0.0.0.0", 0).listen {|serv|
-
# p serv.connect_address #=> #<Addrinfo: 127.0.0.1:53660 TCP>
-
# serv.connect_address.connect {|c|
-
# s, _ = serv.accept
-
# p [c, s] #=> [#<Socket:fd 4>, #<Socket:fd 6>]
-
# }
-
# }
-
#
-
1
def connect_address
-
addr = local_address
-
afamily = addr.afamily
-
if afamily == Socket::AF_INET
-
raise SocketError, "unbound IPv4 socket" if addr.ip_port == 0
-
if addr.ip_address == "0.0.0.0"
-
addr = Addrinfo.new(["AF_INET", addr.ip_port, nil, "127.0.0.1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_INET6) && afamily == Socket::AF_INET6
-
raise SocketError, "unbound IPv6 socket" if addr.ip_port == 0
-
if addr.ip_address == "::"
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "0.0.0.0" # MacOS X 10.4 returns "a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
elsif addr.ip_address == "::ffff:0.0.0.0" # MacOS X 10.6 returns "::ffff:a.b.c.d" for IPv4-mapped IPv6 address.
-
addr = Addrinfo.new(["AF_INET6", addr.ip_port, nil, "::1"], addr.pfamily, addr.socktype, addr.protocol)
-
end
-
elsif defined?(Socket::AF_UNIX) && afamily == Socket::AF_UNIX
-
raise SocketError, "unbound Unix socket" if addr.unix_path == ""
-
end
-
addr
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg(mesg, flags=0, dest_sockaddr=nil, *controls) => numbytes_sent
-
#
-
# sendmsg sends a message using sendmsg(2) system call in blocking manner.
-
#
-
# _mesg_ is a string to send.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_OOB.
-
#
-
# _dest_sockaddr_ is a destination socket address for connection-less socket.
-
# It should be a sockaddr such as a result of Socket.sockaddr_in.
-
# An Addrinfo object can be used too.
-
#
-
# _controls_ is a list of ancillary data.
-
# The element of _controls_ should be Socket::AncillaryData or
-
# 3-elements array.
-
# The 3-element array should contains cmsg_level, cmsg_type and data.
-
#
-
# The return value, _numbytes_sent_ is an integer which is the number of bytes sent.
-
#
-
# sendmsg can be used to implement send_io as follows:
-
#
-
# # use Socket::AncillaryData.
-
# ancdata = Socket::AncillaryData.int(:UNIX, :SOCKET, :RIGHTS, io.fileno)
-
# sock.sendmsg("a", 0, nil, ancdata)
-
#
-
# # use 3-element array.
-
# ancdata = [:SOCKET, :RIGHTS, [io.fileno].pack("i!")]
-
# sock.sendmsg("\0", 0, nil, ancdata)
-
1
def sendmsg(mesg, flags = 0, dest_sockaddr = nil, *controls)
-
__sendmsg(mesg, flags, dest_sockaddr, controls)
-
end
-
-
# call-seq:
-
# basicsocket.sendmsg_nonblock(mesg, flags=0, dest_sockaddr=nil, *controls, opts={}) => numbytes_sent
-
#
-
# sendmsg_nonblock sends a message using sendmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#sendmsg
-
# but the non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that sendmsg_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
1
def sendmsg_nonblock(mesg, flags = 0, dest_sockaddr = nil, *controls,
-
exception: true)
-
__sendmsg_nonblock(mesg, flags, dest_sockaddr, controls, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recv_nonblock(maxlen [, flags [, buf [, options ]]]) => mesg
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The result, _mesg_, is the data received.
-
#
-
# When recvfrom(2) returns 0, Socket#recv_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +buf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# serv = TCPServer.new("127.0.0.1", 0)
-
# af, port, host, addr = serv.addr
-
# c = TCPSocket.new(addr, port)
-
# s = serv.accept
-
# c.send "aaa", 0
-
# begin # emulate blocking recv.
-
# p s.recv_nonblock(10) #=> "aaa"
-
# rescue IO::WaitReadable
-
# IO.select([s])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recv_nonblock_ fails.
-
#
-
# BasicSocket#recv_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recv_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recv_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recv_nonblock(len, flag = 0, str = nil, exception: true)
-
__recv_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg(maxmesglen=nil, flags=0, maxcontrollen=nil, opts={}) => [mesg, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in blocking manner.
-
#
-
# _maxmesglen_ is the maximum length of mesg to receive.
-
#
-
# _flags_ is bitwise OR of MSG_* constants such as Socket::MSG_PEEK.
-
#
-
# _maxcontrollen_ is the maximum length of controls (ancillary data) to receive.
-
#
-
# _opts_ is option hash.
-
# Currently :scm_rights=>bool is the only option.
-
#
-
# :scm_rights option specifies that application expects SCM_RIGHTS control message.
-
# If the value is nil or false, application don't expects SCM_RIGHTS control message.
-
# In this case, recvmsg closes the passed file descriptors immediately.
-
# This is the default behavior.
-
#
-
# If :scm_rights value is neither nil nor false, application expects SCM_RIGHTS control message.
-
# In this case, recvmsg creates IO objects for each file descriptors for
-
# Socket::AncillaryData#unix_rights method.
-
#
-
# The return value is 4-elements array.
-
#
-
# _mesg_ is a string of the received message.
-
#
-
# _sender_addrinfo_ is a sender socket address for connection-less socket.
-
# It is an Addrinfo object.
-
# For connection-oriented socket such as TCP, sender_addrinfo is platform dependent.
-
#
-
# _rflags_ is a flags on the received message which is bitwise OR of MSG_* constants such as Socket::MSG_TRUNC.
-
# It will be nil if the system uses 4.3BSD style old recvmsg system call.
-
#
-
# _controls_ is ancillary data which is an array of Socket::AncillaryData objects such as:
-
#
-
# #<Socket::AncillaryData: AF_UNIX SOCKET RIGHTS 7>
-
#
-
# _maxmesglen_ and _maxcontrollen_ can be nil.
-
# In that case, the buffer will be grown until the message is not truncated.
-
# Internally, MSG_PEEK is used.
-
# Buffer full and MSG_CTRUNC are checked for truncation.
-
#
-
# recvmsg can be used to implement recv_io as follows:
-
#
-
# mesg, sender_sockaddr, rflags, *controls = sock.recvmsg(:scm_rights=>true)
-
# controls.each {|ancdata|
-
# if ancdata.cmsg_is?(:SOCKET, :RIGHTS)
-
# return ancdata.unix_rights[0]
-
# end
-
# }
-
1
def recvmsg(dlen = nil, flags = 0, clen = nil, scm_rights: false)
-
__recvmsg(dlen, flags, clen, scm_rights)
-
end
-
-
# call-seq:
-
# basicsocket.recvmsg_nonblock(maxdatalen=nil, flags=0, maxcontrollen=nil, opts={}) => [data, sender_addrinfo, rflags, *controls]
-
#
-
# recvmsg receives a message using recvmsg(2) system call in non-blocking manner.
-
#
-
# It is similar to BasicSocket#recvmsg
-
# but non-blocking flag is set before the system call
-
# and it doesn't retry the system call.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvmsg_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
1
def recvmsg_nonblock(dlen = nil, flags = 0, clen = nil,
-
scm_rights: false, exception: true)
-
__recvmsg_nonblock(dlen, flags, clen, scm_rights, exception)
-
end
-
-
# Linux-specific optimizations to avoid fcntl for IO#read_nonblock
-
# and IO#write_nonblock using MSG_DONTWAIT
-
# Do other platforms support MSG_DONTWAIT reliably?
-
1
if RUBY_PLATFORM =~ /linux/ && Socket.const_defined?(:MSG_DONTWAIT)
-
1
def read_nonblock(len, str = nil, exception: true) # :nodoc:
-
138
__read_nonblock(len, str, exception)
-
end
-
-
1
def write_nonblock(buf, exception: true) # :nodoc:
-
136
__write_nonblock(buf, exception)
-
end
-
end
-
end
-
-
1
class Socket < BasicSocket
-
# enable the socket option IPV6_V6ONLY if IPV6_V6ONLY is available.
-
1
def ipv6only!
-
if defined? Socket::IPV6_V6ONLY
-
self.setsockopt(:IPV6, :V6ONLY, 1)
-
end
-
end
-
-
# call-seq:
-
# socket.recvfrom_nonblock(maxlen[, flags[, outbuf[, opts]]]) => [mesg, sender_addrinfo]
-
#
-
# Receives up to _maxlen_ bytes from +socket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_addrinfo_, contains protocol-specific address
-
# information of the sender.
-
#
-
# When recvfrom(2) returns 0, Socket#recvfrom_nonblock returns
-
# an empty string as data.
-
# The meaning depends on the socket: EOF on TCP, empty packet on UDP, etc.
-
#
-
# === Parameters
-
# * +maxlen+ - the maximum number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +opts+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# # In one file, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# client, client_addrinfo = socket.accept
-
# begin # emulate blocking recvfrom
-
# pair = client.recvfrom_nonblock(20)
-
# rescue IO::WaitReadable
-
# IO.select([client])
-
# retry
-
# end
-
# data = pair[0].chomp
-
# puts "I only received 20 bytes '#{data}'"
-
# sleep 1
-
# socket.close
-
#
-
# # In another file, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Watch this get cut short!"
-
# socket.close
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# Socket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying
-
# recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, str = nil, exception: true)
-
__recvfrom_nonblock(len, flag, str, exception)
-
end
-
-
# call-seq:
-
# socket.accept_nonblock([options]) => [client_socket, client_addrinfo]
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an array containing the accepted socket
-
# for the incoming connection, _client_socket_,
-
# and an Addrinfo, _client_addrinfo_.
-
#
-
# === Example
-
# # In one script, start this first
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.bind(sockaddr)
-
# socket.listen(5)
-
# begin # emulate blocking accept
-
# client_socket, client_addrinfo = socket.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([socket])
-
# retry
-
# end
-
# puts "The client said, '#{client_socket.readline.chomp}'"
-
# client_socket.puts "Hello from script one!"
-
# socket.close
-
#
-
# # In another script, start this second
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(2200, 'localhost')
-
# socket.connect(sockaddr)
-
# socket.puts "Hello from script 2."
-
# puts "The server said, '#{socket.readline.chomp}'"
-
# socket.close
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to _accept_nonblock_ fails.
-
#
-
# Socket#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
-
# :call-seq:
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
-
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
-
#
-
# creates a new socket object connected to host:port using TCP/IP.
-
#
-
# If local_host:local_port is given,
-
# the socket is bound to it.
-
#
-
# The optional last argument _opts_ is options represented by a hash.
-
# _opts_ may have following options:
-
#
-
# [:connect_timeout] specify the timeout in seconds.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# Socket.tcp("www.ruby-lang.org", 80) {|sock|
-
# sock.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n"
-
# sock.close_write
-
# puts sock.read
-
# }
-
#
-
1
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil) # :yield: socket
-
last_error = nil
-
ret = nil
-
-
local_addr_list = nil
-
if local_host != nil || local_port != nil
-
local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil)
-
end
-
-
Addrinfo.foreach(host, port, nil, :STREAM) {|ai|
-
if local_addr_list
-
local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily }
-
next unless local_addr
-
else
-
local_addr = nil
-
end
-
begin
-
sock = local_addr ?
-
ai.connect_from(local_addr, timeout: connect_timeout) :
-
ai.connect(timeout: connect_timeout)
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
ret = sock
-
break
-
}
-
unless ret
-
if last_error
-
raise last_error
-
else
-
raise SocketError, "no appropriate local address"
-
end
-
end
-
if block_given?
-
begin
-
yield ret
-
ensure
-
ret.close
-
end
-
else
-
ret
-
end
-
end
-
-
# :stopdoc:
-
1
def self.ip_sockets_port0(ai_list, reuseaddr)
-
sockets = []
-
begin
-
sockets.clear
-
port = nil
-
ai_list.each {|ai|
-
begin
-
s = Socket.new(ai.pfamily, ai.socktype, ai.protocol)
-
rescue SystemCallError
-
next
-
end
-
sockets << s
-
s.ipv6only! if ai.ipv6?
-
if reuseaddr
-
s.setsockopt(:SOCKET, :REUSEADDR, 1)
-
end
-
unless port
-
s.bind(ai)
-
port = s.local_address.ip_port
-
else
-
s.bind(ai.family_addrinfo(ai.ip_address, port))
-
end
-
}
-
rescue Errno::EADDRINUSE
-
sockets.each(&:close)
-
retry
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :ip_sockets_port0
-
end
-
-
1
def self.tcp_server_sockets_port0(host)
-
ai_list = Addrinfo.getaddrinfo(host, 0, nil, :STREAM, nil, Socket::AI_PASSIVE)
-
sockets = ip_sockets_port0(ai_list, true)
-
begin
-
sockets.each {|s|
-
s.listen(Socket::SOMAXCONN)
-
}
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
sockets
-
end
-
1
class << self
-
1
private :tcp_server_sockets_port0
-
end
-
# :startdoc:
-
-
# creates TCP/IP server sockets for _host_ and _port_.
-
# _host_ is optional.
-
#
-
# If no block given,
-
# it returns an array of listening sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If _port_ is 0, actual port number is chosen dynamically.
-
# However all sockets in the result has same port number.
-
#
-
# # tcp_server_sockets returns two sockets.
-
# sockets = Socket.tcp_server_sockets(1296)
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
#
-
# # The sockets contains IPv6 and IPv4 sockets.
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:1296 TCP>
-
# # #<Addrinfo: 0.0.0.0:1296 TCP>
-
#
-
# # IPv6 and IPv4 socket has same port number, 53114, even if it is chosen dynamically.
-
# sockets = Socket.tcp_server_sockets(0)
-
# sockets.each {|s| p s.local_address }
-
# #=> #<Addrinfo: [::]:53114 TCP>
-
# # #<Addrinfo: 0.0.0.0:53114 TCP>
-
#
-
# # The block is called with the sockets.
-
# Socket.tcp_server_sockets(0) {|sockets|
-
# p sockets #=> [#<Socket:fd 3>, #<Socket:fd 4>]
-
# }
-
#
-
1
def self.tcp_server_sockets(host=nil, port)
-
if port == 0
-
sockets = tcp_server_sockets_port0(host)
-
else
-
last_error = nil
-
sockets = []
-
begin
-
Addrinfo.foreach(host, port, nil, :STREAM, nil, Socket::AI_PASSIVE) {|ai|
-
begin
-
s = ai.listen
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
rescue Exception
-
sockets.each(&:close)
-
raise
-
end
-
end
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close)
-
end
-
else
-
sockets
-
end
-
end
-
-
# yield socket and client address for each a connection accepted via given sockets.
-
#
-
# The arguments are a list of sockets.
-
# The individual argument should be a socket or an array of sockets.
-
#
-
# This method yields the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
1
def self.accept_loop(*sockets) # :yield: socket, client_addrinfo
-
sockets.flatten!(1)
-
if sockets.empty?
-
raise ArgumentError, "no sockets"
-
end
-
loop {
-
readable, _, _ = IO.select(sockets)
-
readable.each {|r|
-
sock, addr = r.accept_nonblock(exception: false)
-
next if sock == :wait_readable
-
yield sock, addr
-
}
-
}
-
end
-
-
# creates a TCP/IP server on _port_ and calls the block for each connection accepted.
-
# The block is called with a socket and a client_address as an Addrinfo object.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server addresses.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it explicitly.
-
#
-
# This method calls the block sequentially.
-
# It means that the next connection is not accepted until the block returns.
-
# So concurrent mechanism, thread for example, should be used to service multiple clients at a time.
-
#
-
# Note that Addrinfo.getaddrinfo is used to determine the server socket addresses.
-
# When Addrinfo.getaddrinfo returns two or more addresses,
-
# IPv4 and IPv6 address for example,
-
# all of them are used.
-
# Socket.tcp_server_loop succeeds if one socket can be used at least.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
# # Threaded echo server
-
# # It services multiple clients at a time.
-
# # Note that it may accept connections too much.
-
# Socket.tcp_server_loop(16807) {|sock, client_addrinfo|
-
# Thread.new {
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
# }
-
#
-
1
def self.tcp_server_loop(host=nil, port, &b) # :yield: socket, client_addrinfo
-
tcp_server_sockets(host, port) {|sockets|
-
accept_loop(sockets, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_sockets([host, ] port)
-
#
-
# Creates UDP/IP sockets for a UDP server.
-
#
-
# If no block given, it returns an array of sockets.
-
#
-
# If a block is given, the block is called with the sockets.
-
# The value of the block is returned.
-
# The sockets are closed when this method returns.
-
#
-
# If _port_ is zero, some port is chosen.
-
# But the chosen port is used for the all sockets.
-
#
-
# # UDP/IP echo server
-
# Socket.udp_server_sockets(0) {|sockets|
-
# p sockets.first.local_address.ip_port #=> 32963
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
# }
-
#
-
1
def self.udp_server_sockets(host=nil, port)
-
last_error = nil
-
sockets = []
-
-
ipv6_recvpktinfo = nil
-
if defined? Socket::AncillaryData
-
if defined? Socket::IPV6_RECVPKTINFO # RFC 3542
-
ipv6_recvpktinfo = Socket::IPV6_RECVPKTINFO
-
elsif defined? Socket::IPV6_PKTINFO # RFC 2292
-
ipv6_recvpktinfo = Socket::IPV6_PKTINFO
-
end
-
end
-
-
local_addrs = Socket.ip_address_list
-
-
ip_list = []
-
Addrinfo.foreach(host, port, nil, :DGRAM, nil, Socket::AI_PASSIVE) {|ai|
-
if ai.ipv4? && ai.ip_address == "0.0.0.0"
-
local_addrs.each {|a|
-
next unless a.ipv4?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET, :DGRAM, 0);
-
}
-
elsif ai.ipv6? && ai.ip_address == "::" && !ipv6_recvpktinfo
-
local_addrs.each {|a|
-
next unless a.ipv6?
-
ip_list << Addrinfo.new(a.to_sockaddr, :INET6, :DGRAM, 0);
-
}
-
else
-
ip_list << ai
-
end
-
}
-
ip_list.uniq!(&:to_sockaddr)
-
-
if port == 0
-
sockets = ip_sockets_port0(ip_list, false)
-
else
-
ip_list.each {|ip|
-
ai = Addrinfo.udp(ip.ip_address, port)
-
begin
-
s = ai.bind
-
rescue SystemCallError
-
last_error = $!
-
next
-
end
-
sockets << s
-
}
-
if sockets.empty?
-
raise last_error
-
end
-
end
-
-
sockets.each {|s|
-
ai = s.local_address
-
if ipv6_recvpktinfo && ai.ipv6? && ai.ip_address == "::"
-
s.setsockopt(:IPV6, ipv6_recvpktinfo, 1)
-
end
-
}
-
-
if block_given?
-
begin
-
yield sockets
-
ensure
-
sockets.each(&:close) if sockets
-
end
-
else
-
sockets
-
end
-
end
-
-
# :call-seq:
-
# Socket.udp_server_recv(sockets) {|msg, msg_src| ... }
-
#
-
# Receive UDP/IP packets from the given _sockets_.
-
# For each packet received, the block is called.
-
#
-
# The block receives _msg_ and _msg_src_.
-
# _msg_ is a string which is the payload of the received packet.
-
# _msg_src_ is a Socket::UDPSource object which is used for reply.
-
#
-
# Socket.udp_server_loop can be implemented using this method as follows.
-
#
-
# udp_server_sockets(host, port) {|sockets|
-
# loop {
-
# readable, _, _ = IO.select(sockets)
-
# udp_server_recv(readable) {|msg, msg_src| ... }
-
# }
-
# }
-
#
-
1
def self.udp_server_recv(sockets)
-
sockets.each {|r|
-
msg, sender_addrinfo, _, *controls = r.recvmsg_nonblock(exception: false)
-
next if msg == :wait_readable
-
ai = r.local_address
-
if ai.ipv6? and pktinfo = controls.find {|c| c.cmsg_is?(:IPV6, :PKTINFO) }
-
ai = Addrinfo.udp(pktinfo.ipv6_pktinfo_addr.ip_address, ai.ip_port)
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.sendmsg reply_msg, 0, sender_addrinfo, pktinfo
-
}
-
else
-
yield msg, UDPSource.new(sender_addrinfo, ai) {|reply_msg|
-
r.send reply_msg, 0, sender_addrinfo
-
}
-
end
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop_on(sockets) {|msg, msg_src| ... }
-
#
-
# Run UDP/IP server loop on the given sockets.
-
#
-
# The return value of Socket.udp_server_sockets is appropriate for the argument.
-
#
-
# It calls the block for each message received.
-
#
-
1
def self.udp_server_loop_on(sockets, &b) # :yield: msg, msg_src
-
loop {
-
readable, _, _ = IO.select(sockets)
-
udp_server_recv(readable, &b)
-
}
-
end
-
-
# :call-seq:
-
# Socket.udp_server_loop(port) {|msg, msg_src| ... }
-
# Socket.udp_server_loop(host, port) {|msg, msg_src| ... }
-
#
-
# creates a UDP/IP server on _port_ and calls the block for each message arrived.
-
# The block is called with the message and its source information.
-
#
-
# This method allocates sockets internally using _port_.
-
# If _host_ is specified, it is used conjunction with _port_ to determine the server addresses.
-
#
-
# The _msg_ is a string.
-
#
-
# The _msg_src_ is a Socket::UDPSource object.
-
# It is used for reply.
-
#
-
# # UDP/IP echo server.
-
# Socket.udp_server_loop(9261) {|msg, msg_src|
-
# msg_src.reply msg
-
# }
-
#
-
1
def self.udp_server_loop(host=nil, port, &b) # :yield: message, message_source
-
udp_server_sockets(host, port) {|sockets|
-
udp_server_loop_on(sockets, &b)
-
}
-
end
-
-
# UDP/IP address information used by Socket.udp_server_loop.
-
1
class UDPSource
-
# +remote_address+ is an Addrinfo object.
-
#
-
# +local_address+ is an Addrinfo object.
-
#
-
# +reply_proc+ is a Proc used to send reply back to the source.
-
1
def initialize(remote_address, local_address, &reply_proc)
-
@remote_address = remote_address
-
@local_address = local_address
-
@reply_proc = reply_proc
-
end
-
-
# Address of the source
-
1
attr_reader :remote_address
-
-
# Local address
-
1
attr_reader :local_address
-
-
1
def inspect # :nodoc:
-
"\#<#{self.class}: #{@remote_address.inspect_sockaddr} to #{@local_address.inspect_sockaddr}>".dup
-
end
-
-
# Sends the String +msg+ to the source
-
1
def reply(msg)
-
@reply_proc.call msg
-
end
-
end
-
-
# creates a new socket connected to path using UNIX socket socket.
-
#
-
# If a block is given, the block is called with the socket.
-
# The value of the block is returned.
-
# The socket is closed when this method returns.
-
#
-
# If no block is given, the socket is returned.
-
#
-
# # talk to /tmp/sock socket.
-
# Socket.unix("/tmp/sock") {|sock|
-
# t = Thread.new { IO.copy_stream(sock, STDOUT) }
-
# IO.copy_stream(STDIN, sock)
-
# t.join
-
# }
-
#
-
1
def self.unix(path) # :yield: socket
-
addr = Addrinfo.unix(path)
-
sock = addr.connect
-
if block_given?
-
begin
-
yield sock
-
ensure
-
sock.close
-
end
-
else
-
sock
-
end
-
end
-
-
# creates a UNIX server socket on _path_
-
#
-
# If no block given, it returns a listening socket.
-
#
-
# If a block is given, it is called with the socket and the block value is returned.
-
# When the block exits, the socket is closed and the socket file is removed.
-
#
-
# socket = Socket.unix_server_socket("/tmp/s")
-
# p socket #=> #<Socket:fd 3>
-
# p socket.local_address #=> #<Addrinfo: /tmp/s SOCK_STREAM>
-
#
-
# Socket.unix_server_socket("/tmp/sock") {|s|
-
# p s #=> #<Socket:fd 3>
-
# p s.local_address #=> # #<Addrinfo: /tmp/sock SOCK_STREAM>
-
# }
-
#
-
1
def self.unix_server_socket(path)
-
unless unix_socket_abstract_name?(path)
-
begin
-
st = File.lstat(path)
-
rescue Errno::ENOENT
-
end
-
if st&.socket? && st.owned?
-
File.unlink path
-
end
-
end
-
s = Addrinfo.unix(path).listen
-
if block_given?
-
begin
-
yield s
-
ensure
-
s.close
-
unless unix_socket_abstract_name?(path)
-
File.unlink path
-
end
-
end
-
else
-
s
-
end
-
end
-
-
1
class << self
-
1
private
-
-
1
def unix_socket_abstract_name?(path)
-
/linux/ =~ RUBY_PLATFORM && /\A(\0|\z)/ =~ path
-
end
-
end
-
-
# creates a UNIX socket server on _path_.
-
# It calls the block for each socket accepted.
-
#
-
# If _host_ is specified, it is used with _port_ to determine the server ports.
-
#
-
# The socket is *not* closed when the block returns.
-
# So application should close it.
-
#
-
# This method deletes the socket file pointed by _path_ at first if
-
# the file is a socket file and it is owned by the user of the application.
-
# This is safe only if the directory of _path_ is not changed by a malicious user.
-
# So don't use /tmp/malicious-users-directory/socket.
-
# Note that /tmp/socket and /tmp/your-private-directory/socket is safe assuming that /tmp has sticky bit.
-
#
-
# # Sequential echo server.
-
# # It services only one client at a time.
-
# Socket.unix_server_loop("/tmp/sock") {|sock, client_addrinfo|
-
# begin
-
# IO.copy_stream(sock, sock)
-
# ensure
-
# sock.close
-
# end
-
# }
-
#
-
1
def self.unix_server_loop(path, &b) # :yield: socket, client_addrinfo
-
unix_server_socket(path) {|serv|
-
accept_loop(serv, &b)
-
}
-
end
-
-
# call-seq:
-
# socket.connect_nonblock(remote_sockaddr, [options]) => 0
-
#
-
# Requests a connection to be made on the given +remote_sockaddr+ after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# Returns 0 if successful, otherwise an exception is raised.
-
#
-
# === Parameter
-
# # +remote_sockaddr+ - the +struct+ sockaddr contained in a string or Addrinfo object
-
#
-
# === Example:
-
# # Pull down Google's web page
-
# require 'socket'
-
# include Socket::Constants
-
# socket = Socket.new(AF_INET, SOCK_STREAM, 0)
-
# sockaddr = Socket.sockaddr_in(80, 'www.google.com')
-
# begin # emulate blocking connect
-
# socket.connect_nonblock(sockaddr)
-
# rescue IO::WaitWritable
-
# IO.select(nil, [socket]) # wait 3-way handshake completion
-
# begin
-
# socket.connect_nonblock(sockaddr) # check connection failure
-
# rescue Errno::EISCONN
-
# end
-
# end
-
# socket.write("GET / HTTP/1.0\r\n\r\n")
-
# results = socket.read
-
#
-
# Refer to Socket#connect for the exceptions that may be thrown if the call
-
# to _connect_nonblock_ fails.
-
#
-
# Socket#connect_nonblock may raise any error corresponding to connect(2) failure,
-
# including Errno::EINPROGRESS.
-
#
-
# If the exception is Errno::EINPROGRESS,
-
# it is extended by IO::WaitWritable.
-
# So IO::WaitWritable can be used to rescue the exceptions for retrying connect_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that connect_nonblock should not raise an IO::WaitWritable exception, but
-
# return the symbol +:wait_writable+ instead.
-
#
-
# === See
-
# # Socket#connect
-
1
def connect_nonblock(addr, exception: true)
-
__connect_nonblock(addr, exception)
-
end
-
end
-
-
1
class UDPSocket < IPSocket
-
-
# call-seq:
-
# udpsocket.recvfrom_nonblock(maxlen [, flags[, outbuf [, options]]]) => [mesg, sender_inet_addr]
-
#
-
# Receives up to _maxlen_ bytes from +udpsocket+ using recvfrom(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# _flags_ is zero or more of the +MSG_+ options.
-
# The first element of the results, _mesg_, is the data received.
-
# The second element, _sender_inet_addr_, is an array to represent the sender address.
-
#
-
# When recvfrom(2) returns 0,
-
# Socket#recvfrom_nonblock returns an empty string as data.
-
# It means an empty packet.
-
#
-
# === Parameters
-
# * +maxlen+ - the number of bytes to receive from the socket
-
# * +flags+ - zero or more of the +MSG_+ options
-
# * +outbuf+ - destination String buffer
-
# * +options+ - keyword hash, supporting `exception: false`
-
#
-
# === Example
-
# require 'socket'
-
# s1 = UDPSocket.new
-
# s1.bind("127.0.0.1", 0)
-
# s2 = UDPSocket.new
-
# s2.bind("127.0.0.1", 0)
-
# s2.connect(*s1.addr.values_at(3,1))
-
# s1.connect(*s2.addr.values_at(3,1))
-
# s1.send "aaa", 0
-
# begin # emulate blocking recvfrom
-
# p s2.recvfrom_nonblock(10) #=> ["aaa", ["AF_INET", 33302, "localhost.localdomain", "127.0.0.1"]]
-
# rescue IO::WaitReadable
-
# IO.select([s2])
-
# retry
-
# end
-
#
-
# Refer to Socket#recvfrom for the exceptions that may be thrown if the call
-
# to _recvfrom_nonblock_ fails.
-
#
-
# UDPSocket#recvfrom_nonblock may raise any error corresponding to recvfrom(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK or Errno::EAGAIN,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying recvfrom_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that recvfrom_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * Socket#recvfrom
-
1
def recvfrom_nonblock(len, flag = 0, outbuf = nil, exception: true)
-
__recvfrom_nonblock(len, flag, outbuf, exception)
-
end
-
end
-
-
1
class TCPServer < TCPSocket
-
-
# call-seq:
-
# tcpserver.accept_nonblock([options]) => tcpsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted TCPSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = TCPServer.new(2202)
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to TCPServer#accept_nonblock fails.
-
#
-
# TCPServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED, Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * TCPServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
end
-
-
class UNIXServer < UNIXSocket
-
# call-seq:
-
# unixserver.accept_nonblock([options]) => unixsocket
-
#
-
# Accepts an incoming connection using accept(2) after
-
# O_NONBLOCK is set for the underlying file descriptor.
-
# It returns an accepted UNIXSocket for the incoming connection.
-
#
-
# === Example
-
# require 'socket'
-
# serv = UNIXServer.new("/tmp/sock")
-
# begin # emulate blocking accept
-
# sock = serv.accept_nonblock
-
# rescue IO::WaitReadable, Errno::EINTR
-
# IO.select([serv])
-
# retry
-
# end
-
# # sock is an accepted socket.
-
#
-
# Refer to Socket#accept for the exceptions that may be thrown if the call
-
# to UNIXServer#accept_nonblock fails.
-
#
-
# UNIXServer#accept_nonblock may raise any error corresponding to accept(2) failure,
-
# including Errno::EWOULDBLOCK.
-
#
-
# If the exception is Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::ECONNABORTED or Errno::EPROTO,
-
# it is extended by IO::WaitReadable.
-
# So IO::WaitReadable can be used to rescue the exceptions for retrying accept_nonblock.
-
#
-
# By specifying a keyword argument _exception_ to +false+, you can indicate
-
# that accept_nonblock should not raise an IO::WaitReadable exception, but
-
# return the symbol +:wait_readable+ instead.
-
#
-
# === See
-
# * UNIXServer#accept
-
# * Socket#accept
-
1
def accept_nonblock(exception: true)
-
__accept_nonblock(exception)
-
end
-
1
end if defined?(UNIXSocket)
-
# frozen_string_literal: true
-
#
-
# tempfile - manipulates temporary files
-
#
-
# $Id: tempfile.rb 66415 2018-12-16 12:09:08Z nobu $
-
#
-
-
1
require 'delegate'
-
1
require 'tmpdir'
-
-
# A utility class for managing temporary files. When you create a Tempfile
-
# object, it will create a temporary file with a unique filename. A Tempfile
-
# objects behaves just like a File object, and you can perform all the usual
-
# file operations on it: reading data, writing data, changing its permissions,
-
# etc. So although this class does not explicitly document all instance methods
-
# supported by File, you can in fact call any File instance method on a
-
# Tempfile object.
-
#
-
# == Synopsis
-
#
-
# require 'tempfile'
-
#
-
# file = Tempfile.new('foo')
-
# file.path # => A unique filename in the OS's temp directory,
-
# # e.g.: "/tmp/foo.24722.0"
-
# # This filename contains 'foo' in its basename.
-
# file.write("hello world")
-
# file.rewind
-
# file.read # => "hello world"
-
# file.close
-
# file.unlink # deletes the temp file
-
#
-
# == Good practices
-
#
-
# === Explicit close
-
#
-
# When a Tempfile object is garbage collected, or when the Ruby interpreter
-
# exits, its associated temporary file is automatically deleted. This means
-
# that's it's unnecessary to explicitly delete a Tempfile after use, though
-
# it's good practice to do so: not explicitly deleting unused Tempfiles can
-
# potentially leave behind large amounts of tempfiles on the filesystem
-
# until they're garbage collected. The existence of these temp files can make
-
# it harder to determine a new Tempfile filename.
-
#
-
# Therefore, one should always call #unlink or close in an ensure block, like
-
# this:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink after creation
-
#
-
# On POSIX systems, it's possible to unlink a file right after creating it,
-
# and before closing it. This removes the filesystem entry without closing
-
# the file handle, so it ensures that only the processes that already had
-
# the file handle open can access the file's contents. It's strongly
-
# recommended that you do this if you do not want any other processes to
-
# be able to read from or write to the Tempfile, and you do not need to
-
# know the Tempfile's filename either.
-
#
-
# For example, a practical use case for unlink-after-creation would be this:
-
# you need a large byte buffer that's too large to comfortably fit in RAM,
-
# e.g. when you're writing a web server and you want to buffer the client's
-
# file upload data.
-
#
-
# Please refer to #unlink for more information and a code example.
-
#
-
# == Minor notes
-
#
-
# Tempfile's filename picking method is both thread-safe and inter-process-safe:
-
# it guarantees that no other threads or processes will pick the same filename.
-
#
-
# Tempfile itself however may not be entirely thread-safe. If you access the
-
# same Tempfile object from multiple threads then you should protect it with a
-
# mutex.
-
1
class Tempfile < DelegateClass(File)
-
# Creates a temporary file with permissions 0600 (= only readable and
-
# writable by the owner) and opens it with mode "w+".
-
#
-
# The +basename+ parameter is used to determine the name of the
-
# temporary file. You can either pass a String or an Array with
-
# 2 String elements. In the former form, the temporary file's base
-
# name will begin with the given string. In the latter form,
-
# the temporary file's base name will begin with the array's first
-
# element, and end with the second element. For example:
-
#
-
# file = Tempfile.new('hello')
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0"
-
#
-
# # Use the Array form to enforce an extension in the filename:
-
# file = Tempfile.new(['hello', '.jpg'])
-
# file.path # => something like: "/tmp/hello2843-8392-92849382--0.jpg"
-
#
-
# The temporary file will be placed in the directory as specified
-
# by the +tmpdir+ parameter. By default, this is +Dir.tmpdir+.
-
# When $SAFE > 0 and the given +tmpdir+ is tainted, it uses
-
# '/tmp' as the temporary directory. Please note that ENV values
-
# are tainted by default, and +Dir.tmpdir+'s return value might
-
# come from environment variables (e.g. <tt>$TMPDIR</tt>).
-
#
-
# file = Tempfile.new('hello', '/home/aisaka')
-
# file.path # => something like: "/home/aisaka/hello2843-8392-92849382--0"
-
#
-
# You can also pass an options hash. Under the hood, Tempfile creates
-
# the temporary file using +File.open+. These options will be passed to
-
# +File.open+. This is mostly useful for specifying encoding
-
# options, e.g.:
-
#
-
# Tempfile.new('hello', '/home/aisaka', encoding: 'ascii-8bit')
-
#
-
# # You can also omit the 'tmpdir' parameter:
-
# Tempfile.new('hello', encoding: 'ascii-8bit')
-
#
-
# Note: +mode+ keyword argument, as accepted by Tempfile, can only be
-
# numeric, combination of the modes defined in File::Constants.
-
#
-
# === Exceptions
-
#
-
# If Tempfile.new cannot find a unique filename within a limited
-
# number of tries, then it will raise an exception.
-
1
def initialize(basename="", tmpdir=nil, mode: 0, **options)
-
warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
-
-
@unlinked = false
-
@mode = mode|File::RDWR|File::CREAT|File::EXCL
-
::Dir::Tmpname.create(basename, tmpdir, options) do |tmpname, n, opts|
-
opts[:perm] = 0600
-
@tmpfile = File.open(tmpname, @mode, opts)
-
@opts = opts.freeze
-
end
-
ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
-
-
super(@tmpfile)
-
end
-
-
# Opens or reopens the file with mode "r+".
-
1
def open
-
_close
-
mode = @mode & ~(File::CREAT|File::EXCL)
-
@tmpfile = File.open(@tmpfile.path, mode, @opts)
-
__setobj__(@tmpfile)
-
end
-
-
1
def _close # :nodoc:
-
@tmpfile.close
-
end
-
1
protected :_close
-
-
# Closes the file. If +unlink_now+ is true, then the file will be unlinked
-
# (deleted) after closing. Of course, you can choose to later call #unlink
-
# if you do not unlink it now.
-
#
-
# If you don't explicitly unlink the temporary file, the removal
-
# will be delayed until the object is finalized.
-
1
def close(unlink_now=false)
-
_close
-
unlink if unlink_now
-
end
-
-
# Closes and unlinks (deletes) the file. Has the same effect as called
-
# <tt>close(true)</tt>.
-
1
def close!
-
close(true)
-
end
-
-
# Unlinks (deletes) the file from the filesystem. One should always unlink
-
# the file after using it, as is explained in the "Explicit close" good
-
# practice section in the Tempfile overview:
-
#
-
# file = Tempfile.new('foo')
-
# begin
-
# # ...do something with file...
-
# ensure
-
# file.close
-
# file.unlink # deletes the temp file
-
# end
-
#
-
# === Unlink-before-close
-
#
-
# On POSIX systems it's possible to unlink a file before closing it. This
-
# practice is explained in detail in the Tempfile overview (section
-
# "Unlink after creation"); please refer there for more information.
-
#
-
# However, unlink-before-close may not be supported on non-POSIX operating
-
# systems. Microsoft Windows is the most notable case: unlinking a non-closed
-
# file will result in an error, which this method will silently ignore. If
-
# you want to practice unlink-before-close whenever possible, then you should
-
# write code like this:
-
#
-
# file = Tempfile.new('foo')
-
# file.unlink # On Windows this silently fails.
-
# begin
-
# # ... do something with file ...
-
# ensure
-
# file.close! # Closes the file handle. If the file wasn't unlinked
-
# # because #unlink failed, then this method will attempt
-
# # to do so again.
-
# end
-
1
def unlink
-
return if @unlinked
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
rescue Errno::EACCES
-
# may not be able to unlink on Windows; just ignore
-
return
-
end
-
ObjectSpace.undefine_finalizer(self)
-
@unlinked = true
-
end
-
1
alias delete unlink
-
-
# Returns the full path name of the temporary file.
-
# This will be nil if #unlink has been called.
-
1
def path
-
@unlinked ? nil : @tmpfile.path
-
end
-
-
# Returns the size of the temporary file. As a side effect, the IO
-
# buffer is flushed before determining the size.
-
1
def size
-
if !@tmpfile.closed?
-
@tmpfile.size # File#size calls rb_io_flush_raw()
-
else
-
File.size(@tmpfile.path)
-
end
-
end
-
1
alias length size
-
-
# :stopdoc:
-
1
def inspect
-
if closed?
-
"#<#{self.class}:#{path} (closed)>"
-
else
-
"#<#{self.class}:#{path}>"
-
end
-
end
-
-
1
class Remover # :nodoc:
-
1
def initialize(tmpfile)
-
@pid = Process.pid
-
@tmpfile = tmpfile
-
end
-
-
1
def call(*args)
-
return if @pid != Process.pid
-
-
$stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
-
-
@tmpfile.close
-
begin
-
File.unlink(@tmpfile.path)
-
rescue Errno::ENOENT
-
end
-
-
$stderr.puts "done" if $DEBUG
-
end
-
end
-
-
1
class << self
-
# :startdoc:
-
-
# Creates a new Tempfile.
-
#
-
# If no block is given, this is a synonym for Tempfile.new.
-
#
-
# If a block is given, then a Tempfile object will be constructed,
-
# and the block is run with said object as argument. The Tempfile
-
# object will be automatically closed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
-
#
-
# Tempfile.open('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
# # Equivalent:
-
# f = Tempfile.open('foo', '/home/temp')
-
# begin
-
# # ... do something with f ...
-
# ensure
-
# f.close
-
# end
-
1
def open(*args)
-
tempfile = new(*args)
-
-
if block_given?
-
begin
-
yield(tempfile)
-
ensure
-
tempfile.close
-
end
-
else
-
tempfile
-
end
-
end
-
end
-
end
-
-
# Creates a temporary file as usual File object (not Tempfile).
-
# It doesn't use finalizer and delegation.
-
#
-
# If no block is given, this is similar to Tempfile.new except
-
# creating File instead of Tempfile.
-
# The created file is not removed automatically.
-
# You should use File.unlink to remove it.
-
#
-
# If a block is given, then a File object will be constructed,
-
# and the block is invoked with the object as the argument.
-
# The File object will be automatically closed and
-
# the temporary file is removed after the block terminates.
-
# The call returns the value of the block.
-
#
-
# In any case, all arguments (+basename+, +tmpdir+, +mode+, and
-
# <code>**options</code>) will be treated as Tempfile.new.
-
#
-
# Tempfile.create('foo', '/home/temp') do |f|
-
# # ... do something with f ...
-
# end
-
#
-
1
def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
-
tmpfile = nil
-
Dir::Tmpname.create(basename, tmpdir, options) do |tmpname, n, opts|
-
mode |= File::RDWR|File::CREAT|File::EXCL
-
opts[:perm] = 0600
-
tmpfile = File.open(tmpname, mode, opts)
-
end
-
if block_given?
-
begin
-
yield tmpfile
-
ensure
-
unless tmpfile.closed?
-
if File.identical?(tmpfile, tmpfile.path)
-
unlinked = File.unlink tmpfile.path rescue nil
-
end
-
tmpfile.close
-
end
-
unless unlinked
-
begin
-
File.unlink tmpfile.path
-
rescue Errno::ENOENT
-
end
-
end
-
end
-
else
-
tmpfile
-
end
-
end
-
# frozen_string_literal: false
-
# Timeout long-running blocks
-
#
-
# == Synopsis
-
#
-
# require 'timeout'
-
# status = Timeout::timeout(5) {
-
# # Something that should be interrupted if it takes more than 5 seconds...
-
# }
-
#
-
# == Description
-
#
-
# Timeout provides a way to auto-terminate a potentially long-running
-
# operation if it hasn't finished in a fixed amount of time.
-
#
-
# Previous versions didn't use a module for namespacing, however
-
# #timeout is provided for backwards compatibility. You
-
# should prefer Timeout.timeout instead.
-
#
-
# == Copyright
-
#
-
# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
-
# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
-
-
1
module Timeout
-
# Raised by Timeout.timeout when the block times out.
-
1
class Error < RuntimeError
-
1
attr_reader :thread
-
-
1
def self.catch(*args)
-
79
exc = new(*args)
-
79
exc.instance_variable_set(:@thread, Thread.current)
-
158
::Kernel.catch(exc) {yield exc}
-
end
-
-
1
def exception(*)
-
# TODO: use Fiber.current to see if self can be thrown
-
6
if self.thread == Thread.current
-
3
bt = caller
-
begin
-
3
throw(self, bt)
-
rescue UncaughtThrowError
-
end
-
end
-
3
self
-
end
-
end
-
-
# :stopdoc:
-
1
THIS_FILE = /\A#{Regexp.quote(__FILE__)}:/o
-
1
CALLER_OFFSET = ((c = caller[0]) && THIS_FILE =~ c) ? 1 : 0
-
1
private_constant :THIS_FILE, :CALLER_OFFSET
-
# :startdoc:
-
-
# Perform an operation in a block, raising an error if it takes longer than
-
# +sec+ seconds to complete.
-
#
-
# +sec+:: Number of seconds to wait for the block to terminate. Any number
-
# may be used, including Floats to specify fractional seconds. A
-
# value of 0 or +nil+ will execute the block without any timeout.
-
# +klass+:: Exception Class to raise if the block fails to terminate
-
# in +sec+ seconds. Omitting will use the default, Timeout::Error
-
# +message+:: Error message to raise with Exception Class.
-
# Omitting will use the default, "execution expired"
-
#
-
# Returns the result of the block *if* the block completed before
-
# +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
-
#
-
# The exception thrown to terminate the given block cannot be rescued inside
-
# the block unless +klass+ is given explicitly.
-
#
-
# Note that this is both a method of module Timeout, so you can <tt>include
-
# Timeout</tt> into your classes so they have a #timeout method, as well as
-
# a module method, so you can call it directly as Timeout.timeout().
-
1
def timeout(sec, klass = nil, message = nil) #:yield: +sec+
-
147
return yield(sec) if sec == nil or sec.zero?
-
147
message ||= "execution expired".freeze
-
147
from = "from #{caller_locations(1, 1)[0]}" if $DEBUG
-
147
e = Error
-
147
bl = proc do |exception|
-
begin
-
147
x = Thread.current
-
147
y = Thread.start {
-
147
Thread.current.name = from
-
begin
-
147
sleep sec
-
rescue => e
-
x.raise e
-
else
-
3
x.raise exception, message
-
end
-
}
-
147
return yield(sec)
-
ensure
-
147
if y
-
147
y.kill
-
147
y.join # make sure y is dead.
-
end
-
end
-
end
-
147
if klass
-
begin
-
68
bl.call(klass)
-
rescue klass => e
-
bt = e.backtrace
-
end
-
else
-
79
bt = Error.catch(message, &bl)
-
end
-
3
level = -caller(CALLER_OFFSET).size-2
-
3
while THIS_FILE =~ bt[level]
-
12
bt.delete_at(level)
-
end
-
3
raise(e, message, bt)
-
end
-
-
1
module_function :timeout
-
end
-
-
1
def timeout(*args, &block)
-
warn "Object##{__method__} is deprecated, use Timeout.timeout instead.", uplevel: 1
-
Timeout.timeout(*args, &block)
-
end
-
-
# Another name for Timeout::Error, defined for backwards compatibility with
-
# earlier versions of timeout.rb.
-
1
TimeoutError = Timeout::Error
-
1
class Object
-
1
deprecate_constant :TimeoutError
-
end
-
# frozen_string_literal: true
-
#
-
# tmpdir - retrieve temporary directory path
-
#
-
# $Id: tmpdir.rb 66941 2019-01-29 09:19:52Z naruse $
-
#
-
-
1
require 'fileutils'
-
begin
-
1
require 'etc.so'
-
rescue LoadError # rescue LoadError for miniruby
-
end
-
-
1
class Dir
-
-
1
@@systmpdir ||= defined?(Etc.systmpdir) ? Etc.systmpdir : '/tmp'
-
-
##
-
# Returns the operating system's temporary file path.
-
-
1
def self.tmpdir
-
14
if $SAFE > 0
-
@@systmpdir.dup
-
else
-
14
tmp = nil
-
14
[ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], @@systmpdir, '/tmp', '.'].each do |dir|
-
56
next if !dir
-
14
dir = File.expand_path(dir)
-
14
if stat = File.stat(dir) and stat.directory? and stat.writable? and
-
(!stat.world_writable? or stat.sticky?)
-
14
tmp = dir
-
14
break
-
end rescue nil
-
end
-
14
raise ArgumentError, "could not find a temporary directory" unless tmp
-
14
tmp
-
end
-
end
-
-
# Dir.mktmpdir creates a temporary directory.
-
#
-
# The directory is created with 0700 permission.
-
# Application should not change the permission to make the temporary directory accessible from other users.
-
#
-
# The prefix and suffix of the name of the directory is specified by
-
# the optional first argument, <i>prefix_suffix</i>.
-
# - If it is not specified or nil, "d" is used as the prefix and no suffix is used.
-
# - If it is a string, it is used as the prefix and no suffix is used.
-
# - If it is an array, first element is used as the prefix and second element is used as a suffix.
-
#
-
# Dir.mktmpdir {|dir| dir is ".../d..." }
-
# Dir.mktmpdir("foo") {|dir| dir is ".../foo..." }
-
# Dir.mktmpdir(["foo", "bar"]) {|dir| dir is ".../foo...bar" }
-
#
-
# The directory is created under Dir.tmpdir or
-
# the optional second argument <i>tmpdir</i> if non-nil value is given.
-
#
-
# Dir.mktmpdir {|dir| dir is "#{Dir.tmpdir}/d..." }
-
# Dir.mktmpdir(nil, "/var/tmp") {|dir| dir is "/var/tmp/d..." }
-
#
-
# If a block is given,
-
# it is yielded with the path of the directory.
-
# The directory and its contents are removed
-
# using FileUtils.remove_entry before Dir.mktmpdir returns.
-
# The value of the block is returned.
-
#
-
# Dir.mktmpdir {|dir|
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# }
-
#
-
# If a block is not given,
-
# The path of the directory is returned.
-
# In this case, Dir.mktmpdir doesn't remove the directory.
-
#
-
# dir = Dir.mktmpdir
-
# begin
-
# # use the directory...
-
# open("#{dir}/foo", "w") { ... }
-
# ensure
-
# # remove the directory.
-
# FileUtils.remove_entry dir
-
# end
-
#
-
1
def self.mktmpdir(prefix_suffix=nil, *rest)
-
8
base = nil
-
8
path = Tmpname.create(prefix_suffix || "d", *rest) {|path, _, _, d|
-
8
base = d
-
8
mkdir(path, 0700)
-
}
-
8
if block_given?
-
begin
-
8
yield path
-
ensure
-
8
unless base
-
8
stat = File.stat(File.dirname(path))
-
8
if stat.world_writable? and !stat.sticky?
-
raise ArgumentError, "parent directory is world writable but not sticky"
-
end
-
end
-
8
FileUtils.remove_entry path
-
end
-
else
-
path
-
end
-
end
-
-
1
module Tmpname # :nodoc:
-
1
module_function
-
-
1
def tmpdir
-
8
Dir.tmpdir
-
end
-
-
1
def create(basename, tmpdir=nil, max_try: nil, **opts)
-
8
if $SAFE > 0 and tmpdir.tainted?
-
tmpdir = '/tmp'
-
else
-
8
origdir = tmpdir
-
8
tmpdir ||= tmpdir()
-
end
-
8
n = nil
-
8
prefix, suffix = basename
-
8
prefix = (String.try_convert(prefix) or
-
raise ArgumentError, "unexpected prefix: #{prefix.inspect}")
-
8
prefix = prefix.delete("#{File::SEPARATOR}#{File::ALT_SEPARATOR}")
-
8
suffix &&= (String.try_convert(suffix) or
-
raise ArgumentError, "unexpected suffix: #{suffix.inspect}")
-
8
suffix &&= suffix.delete("#{File::SEPARATOR}#{File::ALT_SEPARATOR}")
-
begin
-
8
t = Time.now.strftime("%Y%m%d")
-
8
path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"\
-
"#{n ? %[-#{n}] : ''}#{suffix||''}"
-
8
path = File.join(tmpdir, path)
-
8
yield(path, n, opts, origdir)
-
rescue Errno::EEXIST
-
n ||= 0
-
n += 1
-
retry if !max_try or n < max_try
-
raise "cannot generate temporary name using `#{basename}' under `#{tmpdir}'"
-
end
-
8
path
-
end
-
end
-
end
-
1
require "optparse"
-
1
require "thread"
-
1
require "mutex_m"
-
1
require "minitest/parallel"
-
1
require "stringio"
-
-
##
-
# :include: README.rdoc
-
-
1
module Minitest
-
1
VERSION = "5.13.0" # :nodoc:
-
1
ENCS = "".respond_to? :encoding # :nodoc:
-
-
1
@@installed_at_exit ||= false
-
1
@@after_run = []
-
1
@extensions = []
-
-
2
mc = (class << self; self; end)
-
-
##
-
# Parallel test executor
-
-
1
mc.send :attr_accessor, :parallel_executor
-
-
1
warn "DEPRECATED: use MT_CPU instead of N for parallel test runs" if ENV["N"]
-
1
n_threads = (ENV["MT_CPU"] || ENV["N"] || 2).to_i
-
1
self.parallel_executor = Parallel::Executor.new n_threads
-
-
##
-
# Filter object for backtraces.
-
-
1
mc.send :attr_accessor, :backtrace_filter
-
-
##
-
# Reporter object to be used for all runs.
-
#
-
# NOTE: This accessor is only available during setup, not during runs.
-
-
1
mc.send :attr_accessor, :reporter
-
-
##
-
# Names of known extension plugins.
-
-
1
mc.send :attr_accessor, :extensions
-
-
##
-
# The signal to use for dumping information to STDERR. Defaults to "INFO".
-
-
1
mc.send :attr_accessor, :info_signal
-
1
self.info_signal = "INFO"
-
-
##
-
# Registers Minitest to run at process exit
-
-
1
def self.autorun
-
at_exit {
-
1
next if $! and not ($!.kind_of? SystemExit and $!.success?)
-
-
1
exit_code = nil
-
-
1
pid = Process.pid
-
1
at_exit {
-
1
next if Process.pid != pid
-
1
@@after_run.reverse_each(&:call)
-
1
exit exit_code || false
-
}
-
-
1
exit_code = Minitest.run ARGV
-
1
} unless @@installed_at_exit
-
1
@@installed_at_exit = true
-
end
-
-
##
-
# A simple hook allowing you to run a block of code after everything
-
# is done running. Eg:
-
#
-
# Minitest.after_run { p $debugging_info }
-
-
1
def self.after_run &block
-
@@after_run << block
-
end
-
-
1
def self.init_plugins options # :nodoc:
-
1
self.extensions.each do |name|
-
1
msg = "plugin_#{name}_init"
-
1
send msg, options if self.respond_to? msg
-
end
-
end
-
-
1
def self.load_plugins # :nodoc:
-
1
return unless self.extensions.empty?
-
-
1
seen = {}
-
-
1
require "rubygems" unless defined? Gem
-
-
1
Gem.find_files("minitest/*_plugin.rb").each do |plugin_path|
-
1
name = File.basename plugin_path, "_plugin.rb"
-
-
1
next if seen[name]
-
1
seen[name] = true
-
-
1
require plugin_path
-
1
self.extensions << name
-
end
-
end
-
-
##
-
# This is the top-level run method. Everything starts from here. It
-
# tells each Runnable sub-class to run, and each of those are
-
# responsible for doing whatever they do.
-
#
-
# The overall structure of a run looks like this:
-
#
-
# Minitest.autorun
-
# Minitest.run(args)
-
# Minitest.__run(reporter, options)
-
# Runnable.runnables.each
-
# runnable.run(reporter, options)
-
# self.runnable_methods.each
-
# self.run_one_method(self, runnable_method, reporter)
-
# Minitest.run_one_method(klass, runnable_method)
-
# klass.new(runnable_method).run
-
-
1
def self.run args = []
-
1
self.load_plugins unless args.delete("--no-plugins") || ENV["MT_NO_PLUGINS"]
-
-
1
options = process_args args
-
-
1
reporter = CompositeReporter.new
-
1
reporter << SummaryReporter.new(options[:io], options)
-
1
reporter << ProgressReporter.new(options[:io], options)
-
-
1
self.reporter = reporter # this makes it available to plugins
-
1
self.init_plugins options
-
1
self.reporter = nil # runnables shouldn't depend on the reporter, ever
-
-
1
self.parallel_executor.start if parallel_executor.respond_to?(:start)
-
1
reporter.start
-
begin
-
1
__run reporter, options
-
rescue Interrupt
-
warn "Interrupted. Exiting..."
-
end
-
1
self.parallel_executor.shutdown
-
1
reporter.report
-
-
1
reporter.passed?
-
end
-
-
##
-
# Internal run method. Responsible for telling all Runnable
-
# sub-classes to run.
-
-
1
def self.__run reporter, options
-
29
suites = Runnable.runnables.reject { |s| s.runnable_methods.empty? }.shuffle
-
24
parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
-
# If we run the parallel tests before the serial tests, the parallel tests
-
# could run in parallel with the serial tests. This would be bad because
-
# the serial tests won't lock around Reporter#record. Run the serial tests
-
# first, so that after they complete, the parallel tests will lock when
-
# recording results.
-
24
serial.map { |suite| suite.run reporter, options } +
-
parallel.map { |suite| suite.run reporter, options }
-
end
-
-
1
def self.process_args args = [] # :nodoc:
-
options = {
-
1
:io => $stdout,
-
}
-
1
orig_args = args.dup
-
-
1
OptionParser.new do |opts|
-
1
opts.banner = "minitest options:"
-
1
opts.version = Minitest::VERSION
-
-
1
opts.on "-h", "--help", "Display this help." do
-
puts opts
-
exit
-
end
-
-
1
opts.on "--no-plugins", "Bypass minitest plugin auto-loading (or set $MT_NO_PLUGINS)."
-
-
1
desc = "Sets random seed. Also via env. Eg: SEED=n rake"
-
1
opts.on "-s", "--seed SEED", Integer, desc do |m|
-
options[:seed] = m.to_i
-
end
-
-
1
opts.on "-v", "--verbose", "Verbose. Show progress processing files." do
-
options[:verbose] = true
-
end
-
-
1
opts.on "-n", "--name PATTERN", "Filter run on /regexp/ or string." do |a|
-
options[:filter] = a
-
end
-
-
1
opts.on "-e", "--exclude PATTERN", "Exclude /regexp/ or string from run." do |a|
-
options[:exclude] = a
-
end
-
-
1
unless extensions.empty?
-
1
opts.separator ""
-
1
opts.separator "Known extensions: #{extensions.join(", ")}"
-
-
1
extensions.each do |meth|
-
1
msg = "plugin_#{meth}_options"
-
1
send msg, opts, options if self.respond_to?(msg)
-
end
-
end
-
-
begin
-
1
opts.parse! args
-
rescue OptionParser::InvalidOption => e
-
puts
-
puts e
-
puts
-
puts opts
-
exit 1
-
end
-
-
1
orig_args -= args
-
end
-
-
1
unless options[:seed] then
-
1
srand
-
1
options[:seed] = (ENV["SEED"] || srand).to_i % 0xFFFF
-
1
orig_args << "--seed" << options[:seed].to_s
-
end
-
-
1
srand options[:seed]
-
-
1
options[:args] = orig_args.map { |s|
-
2
s =~ /[\s|&<>$()]/ ? s.inspect : s
-
}.join " "
-
-
1
options
-
end
-
-
1
def self.filter_backtrace bt # :nodoc:
-
backtrace_filter.filter bt
-
end
-
-
##
-
# Represents anything "runnable", like Test, Spec, Benchmark, or
-
# whatever you can dream up.
-
#
-
# Subclasses of this are automatically registered and available in
-
# Runnable.runnables.
-
-
1
class Runnable
-
##
-
# Number of assertions executed in this run.
-
-
1
attr_accessor :assertions
-
-
##
-
# An assertion raised during the run, if any.
-
-
1
attr_accessor :failures
-
-
##
-
# The time it took to run.
-
-
1
attr_accessor :time
-
-
1
def time_it # :nodoc:
-
112
t0 = Minitest.clock_time
-
-
112
yield
-
ensure
-
112
self.time = Minitest.clock_time - t0
-
end
-
-
##
-
# Name of the run.
-
-
1
def name
-
336
@NAME
-
end
-
-
##
-
# Set the name of the run.
-
-
1
def name= o
-
224
@NAME = o
-
end
-
-
##
-
# Returns all instance methods matching the pattern +re+.
-
-
1
def self.methods_matching re
-
51
public_instance_methods(true).grep(re).map(&:to_s)
-
end
-
-
1
def self.reset # :nodoc:
-
1
@@runnables = []
-
end
-
-
1
reset
-
-
##
-
# Responsible for running all runnable methods in a given class,
-
# each in its own instance. Each instance is passed to the
-
# reporter to record.
-
-
1
def self.run reporter, options = {}
-
23
filter = options[:filter] || "/./"
-
23
filter = Regexp.new $1 if filter.is_a?(String) && filter =~ %r%/(.*)/%
-
-
23
filtered_methods = self.runnable_methods.find_all { |m|
-
112
filter === m || filter === "#{self}##{m}"
-
}
-
-
23
exclude = options[:exclude]
-
23
exclude = Regexp.new $1 if exclude =~ %r%/(.*)/%
-
-
23
filtered_methods.delete_if { |m|
-
112
exclude === m || exclude === "#{self}##{m}"
-
}
-
-
23
return if filtered_methods.empty?
-
-
23
with_info_handler reporter do
-
23
filtered_methods.each do |method_name|
-
112
run_one_method self, method_name, reporter
-
end
-
end
-
end
-
-
##
-
# Runs a single method and has the reporter record the result.
-
# This was considered internal API but is factored out of run so
-
# that subclasses can specialize the running of an individual
-
# test. See Minitest::ParallelTest::ClassMethods for an example.
-
-
1
def self.run_one_method klass, method_name, reporter
-
112
reporter.prerecord klass, method_name
-
112
reporter.record Minitest.run_one_method(klass, method_name)
-
end
-
-
1
def self.with_info_handler reporter, &block # :nodoc:
-
23
handler = lambda do
-
unless reporter.passed? then
-
warn "Current results:"
-
warn ""
-
warn reporter.reporters.first
-
warn ""
-
end
-
end
-
-
23
on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
SIGNALS = Signal.list # :nodoc:
-
-
1
def self.on_signal name, action # :nodoc:
-
135
supported = SIGNALS[name]
-
-
old_trap = trap name do
-
old_trap.call if old_trap.respond_to? :call
-
action.call
-
135
end if supported
-
-
135
yield
-
ensure
-
135
trap name, old_trap if supported
-
end
-
-
##
-
# Each subclass of Runnable is responsible for overriding this
-
# method to return all runnable methods. See #methods_matching.
-
-
1
def self.runnable_methods
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns all subclasses of Runnable.
-
-
1
def self.runnables
-
29
@@runnables
-
end
-
-
1
@@marshal_dump_warned = false
-
-
1
def marshal_dump # :nodoc:
-
unless @@marshal_dump_warned then
-
warn ["Minitest::Runnable#marshal_dump is deprecated.",
-
"You might be violating internals. From", caller.first].join " "
-
@@marshal_dump_warned = true
-
end
-
-
[self.name, self.failures, self.assertions, self.time]
-
end
-
-
1
def marshal_load ary # :nodoc:
-
self.name, self.failures, self.assertions, self.time = ary
-
end
-
-
1
def failure # :nodoc:
-
336
self.failures.first
-
end
-
-
1
def initialize name # :nodoc:
-
224
self.name = name
-
224
self.failures = []
-
224
self.assertions = 0
-
end
-
-
##
-
# Runs a single method. Needs to return self.
-
-
1
def run
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns a single character string to print based on the result
-
# of the run. One of <tt>"."</tt>, <tt>"F"</tt>,
-
# <tt>"E"</tt> or <tt>"S"</tt>.
-
-
1
def result_code
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Was this run skipped? See #passed? for more information.
-
-
1
def skipped?
-
raise NotImplementedError, "subclass responsibility"
-
end
-
end
-
-
##
-
# Shared code for anything that can get passed to a Reporter. See
-
# Minitest::Test & Minitest::Result.
-
-
1
module Reportable
-
##
-
# Did this run pass?
-
#
-
# Note: skipped runs are not considered passing, but they don't
-
# cause the process to exit non-zero.
-
-
1
def passed?
-
112
not self.failure
-
end
-
-
##
-
# The location identifier of this test. Depends on a method
-
# existing called class_name.
-
-
1
def location
-
loc = " [#{self.failure.location}]" unless passed? or error?
-
"#{self.class_name}##{self.name}#{loc}"
-
end
-
-
1
def class_name # :nodoc:
-
raise NotImplementedError, "subclass responsibility"
-
end
-
-
##
-
# Returns ".", "F", or "E" based on the result of the run.
-
-
1
def result_code
-
112
self.failure and self.failure.result_code or "."
-
end
-
-
##
-
# Was this run skipped?
-
-
1
def skipped?
-
112
self.failure and Skip === self.failure
-
end
-
-
##
-
# Did this run error?
-
-
1
def error?
-
self.failures.any? { |f| UnexpectedError === f }
-
end
-
end
-
-
##
-
# This represents a test result in a clean way that can be
-
# marshalled over a wire. Tests can do anything they want to the
-
# test instance and can create conditions that cause Marshal.dump to
-
# blow up. By using Result.from(a_test) you can be reasonably sure
-
# that the test result can be marshalled.
-
-
1
class Result < Runnable
-
1
include Minitest::Reportable
-
-
1
undef_method :marshal_dump
-
1
undef_method :marshal_load
-
-
##
-
# The class name of the test result.
-
-
1
attr_accessor :klass
-
-
##
-
# The location of the test method.
-
-
1
attr_accessor :source_location
-
-
##
-
# Create a new test result from a Runnable instance.
-
-
1
def self.from runnable
-
112
o = runnable
-
-
112
r = self.new o.name
-
112
r.klass = o.class.name
-
112
r.assertions = o.assertions
-
112
r.failures = o.failures.dup
-
112
r.time = o.time
-
-
112
r.source_location = o.method(o.name).source_location rescue ["unknown", -1]
-
-
112
r
-
end
-
-
1
def class_name # :nodoc:
-
self.klass # for Minitest::Reportable
-
end
-
-
1
def to_s # :nodoc:
-
return location if passed? and not skipped?
-
-
failures.map { |failure|
-
"#{failure.result_label}:\n#{self.location}:\n#{failure.message}\n"
-
}.join "\n"
-
end
-
end
-
-
##
-
# Defines the API for Reporters. Subclass this and override whatever
-
# you want. Go nuts.
-
-
1
class AbstractReporter
-
1
include Mutex_m
-
-
##
-
# Starts reporting on the run.
-
-
1
def start
-
end
-
-
##
-
# About to start running a test. This allows a reporter to show
-
# that it is starting or that we are in the middle of a test run.
-
-
1
def prerecord klass, name
-
end
-
-
##
-
# Output and record the result of the test. Call
-
# {result#result_code}[rdoc-ref:Runnable#result_code] to get the
-
# result character string. Stores the result of the run if the run
-
# did not pass.
-
-
1
def record result
-
end
-
-
##
-
# Outputs the summary of the run.
-
-
1
def report
-
end
-
-
##
-
# Did this run pass?
-
-
1
def passed?
-
1
true
-
end
-
end
-
-
1
class Reporter < AbstractReporter # :nodoc:
-
##
-
# The IO used to report.
-
-
1
attr_accessor :io
-
-
##
-
# Command-line options for this run.
-
-
1
attr_accessor :options
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
2
super()
-
2
self.io = io
-
2
self.options = options
-
end
-
end
-
-
##
-
# A very simple reporter that prints the "dots" during the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class ProgressReporter < Reporter
-
1
def prerecord klass, name #:nodoc:
-
112
if options[:verbose] then
-
io.print "%s#%s = " % [klass.name, name]
-
io.flush
-
end
-
end
-
-
1
def record result # :nodoc:
-
112
io.print "%.2f s = " % [result.time] if options[:verbose]
-
112
io.print result.result_code
-
112
io.puts if options[:verbose]
-
end
-
end
-
-
##
-
# A reporter that gathers statistics about a test run. Does not do
-
# any IO because meant to be used as a parent class for a reporter
-
# that does.
-
#
-
# If you want to create an entirely different type of output (eg,
-
# CI, HTML, etc), this is the place to start.
-
#
-
# Example:
-
#
-
# class JenkinsCIReporter < StatisticsReporter
-
# def report
-
# super # Needed to calculate some statistics
-
#
-
# print "<testsuite "
-
# print "tests='#{count}' "
-
# print "failures='#{failures}' "
-
# # Remaining XML...
-
# end
-
# end
-
-
1
class StatisticsReporter < Reporter
-
##
-
# Total number of assertions.
-
-
1
attr_accessor :assertions
-
-
##
-
# Total number of test cases.
-
-
1
attr_accessor :count
-
-
##
-
# An +Array+ of test cases that failed or were skipped.
-
-
1
attr_accessor :results
-
-
##
-
# Time the test run started. If available, the monotonic clock is
-
# used and this is a +Float+, otherwise it's an instance of
-
# +Time+.
-
-
1
attr_accessor :start_time
-
-
##
-
# Test run time. If available, the monotonic clock is used and
-
# this is a +Float+, otherwise it's an instance of +Time+.
-
-
1
attr_accessor :total_time
-
-
##
-
# Total number of tests that failed.
-
-
1
attr_accessor :failures
-
-
##
-
# Total number of tests that erred.
-
-
1
attr_accessor :errors
-
-
##
-
# Total number of tests that where skipped.
-
-
1
attr_accessor :skips
-
-
1
def initialize io = $stdout, options = {} # :nodoc:
-
1
super
-
-
1
self.assertions = 0
-
1
self.count = 0
-
1
self.results = []
-
1
self.start_time = nil
-
1
self.total_time = nil
-
1
self.failures = nil
-
1
self.errors = nil
-
1
self.skips = nil
-
end
-
-
1
def passed? # :nodoc:
-
1
results.all?(&:skipped?)
-
end
-
-
1
def start # :nodoc:
-
1
self.start_time = Minitest.clock_time
-
end
-
-
1
def record result # :nodoc:
-
112
self.count += 1
-
112
self.assertions += result.assertions
-
-
112
results << result if not result.passed? or result.skipped?
-
end
-
-
##
-
# Report on the tracked statistics.
-
-
1
def report
-
1
aggregate = results.group_by { |r| r.failure.class }
-
1
aggregate.default = [] # dumb. group_by should provide this
-
-
1
self.total_time = Minitest.clock_time - start_time
-
1
self.failures = aggregate[Assertion].size
-
1
self.errors = aggregate[UnexpectedError].size
-
1
self.skips = aggregate[Skip].size
-
end
-
end
-
-
##
-
# A reporter that prints the header, summary, and failure details at
-
# the end of the run.
-
#
-
# This is added to the top-level CompositeReporter at the start of
-
# the run. If you want to change the output of minitest via a
-
# plugin, pull this out of the composite and replace it with your
-
# own.
-
-
1
class SummaryReporter < StatisticsReporter
-
# :stopdoc:
-
1
attr_accessor :sync
-
1
attr_accessor :old_sync
-
# :startdoc:
-
-
1
def start # :nodoc:
-
1
super
-
-
1
io.puts "Run options: #{options[:args]}"
-
1
io.puts
-
1
io.puts "# Running:"
-
1
io.puts
-
-
1
self.sync = io.respond_to? :"sync=" # stupid emacs
-
1
self.old_sync, io.sync = io.sync, true if self.sync
-
end
-
-
1
def report # :nodoc:
-
1
super
-
-
1
io.sync = self.old_sync
-
-
1
io.puts unless options[:verbose] # finish the dots
-
1
io.puts
-
1
io.puts statistics
-
1
aggregated_results io
-
1
io.puts summary
-
end
-
-
1
def statistics # :nodoc:
-
1
"Finished in %.6fs, %.4f runs/s, %.4f assertions/s." %
-
[total_time, count / total_time, assertions / total_time]
-
end
-
-
1
def aggregated_results io # :nodoc:
-
1
filtered_results = results.dup
-
1
filtered_results.reject!(&:skipped?) unless options[:verbose]
-
-
1
filtered_results.each_with_index { |result, i|
-
io.puts "\n%3d) %s" % [i+1, result]
-
}
-
1
io.puts
-
1
io
-
end
-
-
1
def to_s # :nodoc:
-
aggregated_results(StringIO.new(binary_string)).string
-
end
-
-
1
def summary # :nodoc:
-
1
extra = ""
-
-
extra = "\n\nYou have skipped tests. Run with --verbose for details." if
-
1
results.any?(&:skipped?) unless options[:verbose] or ENV["MT_NO_SKIP_MSG"]
-
-
1
"%d runs, %d assertions, %d failures, %d errors, %d skips%s" %
-
[count, assertions, failures, errors, skips, extra]
-
end
-
-
1
private
-
-
1
if '<3'.respond_to? :b
-
1
def binary_string; ''.b; end
-
else
-
def binary_string; ''.force_encoding(Encoding::ASCII_8BIT); end
-
end
-
end
-
-
##
-
# Dispatch to multiple reporters as one.
-
-
1
class CompositeReporter < AbstractReporter
-
##
-
# The list of reporters to dispatch to.
-
-
1
attr_accessor :reporters
-
-
1
def initialize *reporters # :nodoc:
-
1
super()
-
1
self.reporters = reporters
-
end
-
-
1
def io # :nodoc:
-
reporters.first.io
-
end
-
-
##
-
# Add another reporter to the mix.
-
-
1
def << reporter
-
2
self.reporters << reporter
-
end
-
-
1
def passed? # :nodoc:
-
1
self.reporters.all?(&:passed?)
-
end
-
-
1
def start # :nodoc:
-
1
self.reporters.each(&:start)
-
end
-
-
1
def prerecord klass, name # :nodoc:
-
112
self.reporters.each do |reporter|
-
# TODO: remove conditional for minitest 6
-
224
reporter.prerecord klass, name if reporter.respond_to? :prerecord
-
end
-
end
-
-
1
def record result # :nodoc:
-
112
self.reporters.each do |reporter|
-
224
reporter.record result
-
end
-
end
-
-
1
def report # :nodoc:
-
1
self.reporters.each(&:report)
-
end
-
end
-
-
##
-
# Represents run failures.
-
-
1
class Assertion < Exception
-
1
def error # :nodoc:
-
self
-
end
-
-
##
-
# Where was this run before an assertion was raised?
-
-
1
def location
-
last_before_assertion = ""
-
self.backtrace.reverse_each do |s|
-
break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
-
last_before_assertion = s
-
end
-
last_before_assertion.sub(/:in .*$/, "")
-
end
-
-
1
def result_code # :nodoc:
-
result_label[0, 1]
-
end
-
-
1
def result_label # :nodoc:
-
"Failure"
-
end
-
end
-
-
##
-
# Assertion raised when skipping a run.
-
-
1
class Skip < Assertion
-
1
def result_label # :nodoc:
-
"Skipped"
-
end
-
end
-
-
##
-
# Assertion wrapping an unexpected error that was raised during a run.
-
-
1
class UnexpectedError < Assertion
-
1
attr_accessor :exception # :nodoc:
-
-
1
def initialize exception # :nodoc:
-
super "Unexpected exception"
-
self.exception = exception
-
end
-
-
1
def backtrace # :nodoc:
-
self.exception.backtrace
-
end
-
-
1
def error # :nodoc:
-
self.exception
-
end
-
-
1
def message # :nodoc:
-
bt = Minitest.filter_backtrace(self.backtrace).join "\n "
-
"#{self.exception.class}: #{self.exception.message}\n #{bt}"
-
end
-
-
1
def result_label # :nodoc:
-
"Error"
-
end
-
end
-
-
##
-
# Provides a simple set of guards that you can use in your tests
-
# to skip execution if it is not applicable. These methods are
-
# mixed into Test as both instance and class methods so you
-
# can use them inside or outside of the test methods.
-
#
-
# def test_something_for_mri
-
# skip "bug 1234" if jruby?
-
# # ...
-
# end
-
#
-
# if windows? then
-
# # ... lots of test methods ...
-
# end
-
-
1
module Guard
-
-
##
-
# Is this running on jruby?
-
-
1
def jruby? platform = RUBY_PLATFORM
-
"java" == platform
-
end
-
-
##
-
# Is this running on maglev?
-
-
1
def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `maglev?` called from #{where}. This will fail in Minitest 6."
-
"maglev" == platform
-
end
-
-
##
-
# Is this running on mri?
-
-
1
def mri? platform = RUBY_DESCRIPTION
-
/^ruby/ =~ platform
-
end
-
-
##
-
# Is this running on macOS?
-
-
1
def osx? platform = RUBY_PLATFORM
-
/darwin/ =~ platform
-
end
-
-
##
-
# Is this running on rubinius?
-
-
1
def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: `rubinius?` called from #{where}. This will fail in Minitest 6."
-
"rbx" == platform
-
end
-
-
##
-
# Is this running on windows?
-
-
1
def windows? platform = RUBY_PLATFORM
-
/mswin|mingw/ =~ platform
-
end
-
end
-
-
##
-
# The standard backtrace filter for minitest.
-
#
-
# See Minitest.backtrace_filter=.
-
-
1
class BacktraceFilter
-
-
1
MT_RE = %r%lib/minitest% #:nodoc:
-
-
##
-
# Filter +bt+ to something useful. Returns the whole thing if $DEBUG.
-
-
1
def filter bt
-
return ["No backtrace"] unless bt
-
-
return bt.dup if $DEBUG
-
-
new_bt = bt.take_while { |line| line !~ MT_RE }
-
new_bt = bt.select { |line| line !~ MT_RE } if new_bt.empty?
-
new_bt = bt.dup if new_bt.empty?
-
-
new_bt
-
end
-
end
-
-
1
self.backtrace_filter = BacktraceFilter.new
-
-
1
def self.run_one_method klass, method_name # :nodoc:
-
112
result = klass.new(method_name).run
-
112
raise "#{klass}#run _must_ return a Result" unless Result === result
-
112
result
-
end
-
-
# :stopdoc:
-
-
1
if defined? Process::CLOCK_MONOTONIC # :nodoc:
-
1
def self.clock_time
-
338
Process.clock_gettime Process::CLOCK_MONOTONIC
-
end
-
else
-
def self.clock_time
-
Time.now
-
end
-
end
-
-
1
class Runnable # re-open
-
1
def self.inherited klass # :nodoc:
-
28
self.runnables << klass
-
28
super
-
end
-
end
-
-
# :startdoc:
-
end
-
-
1
require "minitest/test"
-
# encoding: UTF-8
-
-
1
require "rbconfig"
-
1
require "tempfile"
-
1
require "stringio"
-
-
1
module Minitest
-
##
-
# Minitest Assertions. All assertion methods accept a +msg+ which is
-
# printed if the assertion fails.
-
#
-
# Protocol: Nearly everything here boils up to +assert+, which
-
# expects to be able to increment an instance accessor named
-
# +assertions+. This is not provided by Assertions and must be
-
# provided by the thing including Assertions. See Minitest::Runnable
-
# for an example.
-
-
1
module Assertions
-
1
UNDEFINED = Object.new # :nodoc:
-
-
1
def UNDEFINED.inspect # :nodoc:
-
"UNDEFINED" # again with the rdoc bugs... :(
-
end
-
-
##
-
# Returns the diff command to use in #diff. Tries to intelligently
-
# figure out what diff to use.
-
-
1
def self.diff
-
return @diff if defined? @diff
-
-
@diff = if (RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ &&
-
system("diff.exe", __FILE__, __FILE__)) then
-
"diff.exe -u"
-
elsif system("gdiff", __FILE__, __FILE__)
-
"gdiff -u" # solaris and kin suck
-
elsif system("diff", __FILE__, __FILE__)
-
"diff -u"
-
else
-
nil
-
end
-
end
-
-
##
-
# Set the diff command to use in #diff.
-
-
1
def self.diff= o
-
@diff = o
-
end
-
-
##
-
# Returns a diff between +exp+ and +act+. If there is no known
-
# diff command or if it doesn't make sense to diff the output
-
# (single line, short output), then it simply returns a basic
-
# comparison between the two.
-
#
-
# See +things_to_diff+ for more info.
-
-
1
def diff exp, act
-
result = nil
-
-
expect, butwas = things_to_diff(exp, act)
-
-
return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless
-
expect
-
-
Tempfile.open("expect") do |a|
-
a.puts expect
-
a.flush
-
-
Tempfile.open("butwas") do |b|
-
b.puts butwas
-
b.flush
-
-
result = `#{Minitest::Assertions.diff} #{a.path} #{b.path}`
-
result.sub!(/^\-\-\- .+/, "--- expected")
-
result.sub!(/^\+\+\+ .+/, "+++ actual")
-
-
if result.empty? then
-
klass = exp.class
-
result = [
-
"No visible difference in the #{klass}#inspect output.\n",
-
"You should look at the implementation of #== on ",
-
"#{klass} or its members.\n",
-
expect,
-
].join
-
end
-
end
-
end
-
-
result
-
end
-
-
##
-
# Returns things to diff [expect, butwas], or [nil, nil] if nothing to diff.
-
#
-
# Criterion:
-
#
-
# 1. Strings include newlines or escaped newlines, but not both.
-
# 2. or: String lengths are > 30 characters.
-
# 3. or: Strings are equal to each other (but maybe different encodings?).
-
# 4. and: we found a diff executable.
-
-
1
def things_to_diff exp, act
-
expect = mu_pp_for_diff exp
-
butwas = mu_pp_for_diff act
-
-
e1, e2 = expect.include?("\n"), expect.include?("\\n")
-
b1, b2 = butwas.include?("\n"), butwas.include?("\\n")
-
-
need_to_diff =
-
(e1 ^ e2 ||
-
b1 ^ b2 ||
-
expect.size > 30 ||
-
butwas.size > 30 ||
-
expect == butwas) &&
-
Minitest::Assertions.diff
-
-
need_to_diff && [expect, butwas]
-
end
-
-
##
-
# This returns a human-readable version of +obj+. By default
-
# #inspect is called. You can override this to use #pretty_inspect
-
# if you want.
-
#
-
# See Minitest::Test.make_my_diffs_pretty!
-
-
1
def mu_pp obj
-
s = obj.inspect
-
-
if defined? Encoding then
-
s = s.encode Encoding.default_external
-
-
if String === obj && (obj.encoding != Encoding.default_external ||
-
!obj.valid_encoding?) then
-
enc = "# encoding: #{obj.encoding}"
-
val = "# valid: #{obj.valid_encoding?}"
-
s = "#{enc}\n#{val}\n#{s}"
-
end
-
end
-
-
s
-
end
-
-
##
-
# This returns a diff-able more human-readable version of +obj+.
-
# This differs from the regular mu_pp because it expands escaped
-
# newlines and makes hex-values (like object_ids) generic. This
-
# uses mu_pp to do the first pass and then cleans it up.
-
-
1
def mu_pp_for_diff obj
-
str = mu_pp obj
-
-
# both '\n' & '\\n' (_after_ mu_pp (aka inspect))
-
single = !!str.match(/(?<!\\|^)\\n/)
-
double = !!str.match(/(?<=\\|^)\\n/)
-
-
process =
-
if single ^ double then
-
if single then
-
lambda { |s| s == "\\n" ? "\n" : s } # unescape
-
else
-
lambda { |s| s == "\\\\n" ? "\\n\n" : s } # unescape a bit, add nls
-
end
-
else
-
:itself # leave it alone
-
end
-
-
str.
-
gsub(/\\?\\n/, &process).
-
gsub(/:0x[a-fA-F0-9]{4,}/m, ":0xXXXXXX") # anonymize hex values
-
end
-
-
##
-
# Fails unless +test+ is truthy.
-
-
1
def assert test, msg = nil
-
1073
self.assertions += 1
-
1073
unless test then
-
msg ||= "Expected #{mu_pp test} to be truthy."
-
msg = msg.call if Proc === msg
-
raise Minitest::Assertion, msg
-
end
-
1073
true
-
end
-
-
1
def _synchronize # :nodoc:
-
yield
-
end
-
-
##
-
# Fails unless +obj+ is empty.
-
-
1
def assert_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" }
-
assert_respond_to obj, :empty?
-
assert obj.empty?, msg
-
end
-
-
1
E = "" # :nodoc:
-
-
##
-
# Fails unless <tt>exp == act</tt> printing the difference between
-
# the two, if possible.
-
#
-
# If there is no visible difference but the assertion fails, you
-
# should suspect that your #== is buggy, or your inspect output is
-
# missing crucial details. For nicer structural diffing, set
-
# Minitest::Test.make_my_diffs_pretty!
-
#
-
# For floats use assert_in_delta.
-
#
-
# See also: Minitest::Assertions.diff
-
-
1
def assert_equal exp, act, msg = nil
-
596
msg = message(msg, E) { diff exp, act }
-
596
result = assert exp == act, msg
-
-
596
if nil == exp then
-
if Minitest::VERSION =~ /^6/ then
-
refute_nil exp, "Use assert_nil if expecting nil."
-
else
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
-
warn "DEPRECATED: Use assert_nil if expecting nil from #{where}. This will fail in Minitest 6."
-
end
-
end
-
-
596
result
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ are within +delta+
-
# of each other.
-
#
-
# assert_in_delta Math::PI, (22.0 / 7.0), 0.01
-
-
1
def assert_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
-
}
-
assert delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails unless +exp+ and +act+ have a relative
-
# error less than +epsilon+.
-
-
1
def assert_in_epsilon exp, act, epsilon = 0.001, msg = nil
-
assert_in_delta exp, act, [exp.abs, act.abs].min * epsilon, msg
-
end
-
-
##
-
# Fails unless +collection+ includes +obj+.
-
-
1
def assert_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
assert collection.include?(obj), msg
-
end
-
-
##
-
# Fails unless +obj+ is an instance of +cls+.
-
-
1
def assert_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}"
-
}
-
-
assert obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails unless +obj+ is a kind of +cls+.
-
-
1
def assert_kind_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" }
-
-
assert obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails unless +matcher+ <tt>=~</tt> +obj+.
-
-
1
def assert_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
assert matcher =~ obj, msg
-
end
-
-
##
-
# Fails unless +obj+ is nil
-
-
1
def assert_nil obj, msg = nil
-
13
msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" }
-
13
assert obj.nil?, msg
-
end
-
-
##
-
# For testing with binary operators. Eg:
-
#
-
# assert_operator 5, :<=, 4
-
-
1
def assert_operator o1, op, o2 = UNDEFINED, msg = nil
-
return assert_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" }
-
assert o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if stdout or stderr do not output the expected results.
-
# Pass in nil if you don't care about that streams output. Pass in
-
# "" if you require it to be silent. Pass in a regexp if you want
-
# to pattern match.
-
#
-
# assert_output(/hey/) { method_with_output }
-
#
-
# NOTE: this uses #capture_io, not #capture_subprocess_io.
-
#
-
# See also: #assert_silent
-
-
1
def assert_output stdout = nil, stderr = nil
-
flunk "assert_output requires a block to capture output." unless
-
block_given?
-
-
out, err = capture_io do
-
yield
-
end
-
-
err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr
-
out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout
-
-
y = send err_msg, stderr, err, "In stderr" if err_msg
-
x = send out_msg, stdout, out, "In stdout" if out_msg
-
-
(!stdout || x) && (!stderr || y)
-
end
-
-
##
-
# Fails unless +path+ exists.
-
-
1
def assert_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to exist" }
-
assert File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates. Eg:
-
#
-
# assert_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by assert_operator:
-
#
-
# str.must_be :empty?
-
-
1
def assert_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" }
-
assert o1.__send__(op), msg
-
end
-
-
##
-
# Fails unless the block raises one of +exp+. Returns the
-
# exception matched so you can check the message, attributes, etc.
-
#
-
# +exp+ takes an optional message on the end to help explain
-
# failures and defaults to StandardError if no exception class is
-
# passed. Eg:
-
#
-
# assert_raises(CustomError) { method_with_custom_error }
-
#
-
# With custom error message:
-
#
-
# assert_raises(CustomError, 'This should have raised CustomError') { method_with_custom_error }
-
#
-
# Using the returned object:
-
#
-
# error = assert_raises(CustomError) do
-
# raise CustomError, 'This is really bad'
-
# end
-
#
-
# assert_equal 'This is really bad', error.message
-
-
1
def assert_raises *exp
-
flunk "assert_raises requires a block to capture errors." unless
-
8
block_given?
-
-
8
msg = "#{exp.pop}.\n" if String === exp.last
-
8
exp << StandardError if exp.empty?
-
-
begin
-
8
yield
-
rescue *exp => e
-
8
pass # count assertion
-
8
return e
-
rescue Minitest::Skip, Minitest::Assertion
-
# don't count assertion
-
raise
-
rescue SignalException, SystemExit
-
raise
-
rescue Exception => e
-
flunk proc {
-
exception_details(e, "#{msg}#{mu_pp(exp)} exception expected, not")
-
}
-
end
-
-
exp = exp.first if exp.size == 1
-
-
flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised."
-
end
-
-
##
-
# Fails unless +obj+ responds to +meth+.
-
-
1
def assert_respond_to obj, meth, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}"
-
}
-
assert obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails unless +exp+ and +act+ are #equal?
-
-
1
def assert_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to be the same as %s (oid=%d)" % data
-
}
-
assert exp.equal?(act), msg
-
end
-
-
##
-
# +send_ary+ is a receiver, message and arguments.
-
#
-
# Fails unless the call returns a true value
-
-
1
def assert_send send_ary, m = nil
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: assert_send. From #{where}"
-
-
recv, msg, *args = send_ary
-
m = message(m) {
-
"Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" }
-
assert recv.__send__(msg, *args), m
-
end
-
-
##
-
# Fails if the block outputs anything to stderr or stdout.
-
#
-
# See also: #assert_output
-
-
1
def assert_silent
-
assert_output "", "" do
-
yield
-
end
-
end
-
-
##
-
# Fails unless the block throws +sym+
-
-
1
def assert_throws sym, msg = nil
-
default = "Expected #{mu_pp(sym)} to have been thrown"
-
caught = true
-
catch(sym) do
-
begin
-
yield
-
rescue ThreadError => e # wtf?!? 1.8 + threads == suck
-
default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}"
-
rescue ArgumentError => e # 1.9 exception
-
raise e unless e.message.include?("uncaught throw")
-
default += ", not #{e.message.split(/ /).last}"
-
rescue NameError => e # 1.8 exception
-
raise e unless e.name == sym
-
default += ", not #{e.name.inspect}"
-
end
-
caught = false
-
end
-
-
assert caught, message(msg) { default }
-
end
-
-
##
-
# Captures $stdout and $stderr into strings:
-
#
-
# out, err = capture_io do
-
# puts "Some info"
-
# warn "You did a bad thing"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: For efficiency, this method uses StringIO and does not
-
# capture IO for subprocesses. Use #capture_subprocess_io for
-
# that.
-
-
1
def capture_io
-
_synchronize do
-
begin
-
captured_stdout, captured_stderr = StringIO.new, StringIO.new
-
-
orig_stdout, orig_stderr = $stdout, $stderr
-
$stdout, $stderr = captured_stdout, captured_stderr
-
-
yield
-
-
return captured_stdout.string, captured_stderr.string
-
ensure
-
$stdout = orig_stdout
-
$stderr = orig_stderr
-
end
-
end
-
end
-
-
##
-
# Captures $stdout and $stderr into strings, using Tempfile to
-
# ensure that subprocess IO is captured as well.
-
#
-
# out, err = capture_subprocess_io do
-
# system "echo Some info"
-
# system "echo You did a bad thing 1>&2"
-
# end
-
#
-
# assert_match %r%info%, out
-
# assert_match %r%bad%, err
-
#
-
# NOTE: This method is approximately 10x slower than #capture_io so
-
# only use it when you need to test the output of a subprocess.
-
-
1
def capture_subprocess_io
-
_synchronize do
-
begin
-
require "tempfile"
-
-
captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err")
-
-
orig_stdout, orig_stderr = $stdout.dup, $stderr.dup
-
$stdout.reopen captured_stdout
-
$stderr.reopen captured_stderr
-
-
yield
-
-
$stdout.rewind
-
$stderr.rewind
-
-
return captured_stdout.read, captured_stderr.read
-
ensure
-
captured_stdout.unlink
-
captured_stderr.unlink
-
$stdout.reopen orig_stdout
-
$stderr.reopen orig_stderr
-
end
-
end
-
end
-
-
##
-
# Returns details for exception +e+
-
-
1
def exception_details e, msg
-
[
-
"#{msg}",
-
"Class: <#{e.class}>",
-
"Message: <#{e.message.inspect}>",
-
"---Backtrace---",
-
"#{Minitest.filter_backtrace(e.backtrace).join("\n")}",
-
"---------------",
-
].join "\n"
-
end
-
-
##
-
# Fails after a given date (in the local time zone). This allows
-
# you to put time-bombs in your tests if you need to keep
-
# something around until a later date lest you forget about it.
-
-
1
def fail_after y,m,d,msg
-
flunk msg if Time.now > Time.local(y, m, d)
-
end
-
-
##
-
# Fails with +msg+.
-
-
1
def flunk msg = nil
-
msg ||= "Epic Fail!"
-
assert false, msg
-
end
-
-
##
-
# Returns a proc that will output +msg+ along with the default message.
-
-
1
def message msg = nil, ending = nil, &default
-
798
proc {
-
msg = msg.call.chomp(".") if Proc === msg
-
custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty?
-
"#{custom_message}#{default.call}#{ending || "."}"
-
}
-
end
-
-
##
-
# used for counting assertions
-
-
1
def pass _msg = nil
-
8
assert true
-
end
-
-
##
-
# Fails if +test+ is truthy.
-
-
1
def refute test, msg = nil
-
250
msg ||= message { "Expected #{mu_pp(test)} to not be truthy" }
-
250
not assert !test, msg
-
end
-
-
##
-
# Fails if +obj+ is empty.
-
-
1
def refute_empty obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" }
-
assert_respond_to obj, :empty?
-
refute obj.empty?, msg
-
end
-
-
##
-
# Fails if <tt>exp == act</tt>.
-
#
-
# For floats use refute_in_delta.
-
-
1
def refute_equal exp, act, msg = nil
-
8
msg = message(msg) {
-
"Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}"
-
}
-
8
refute exp == act, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ is within +delta+ of +act+.
-
#
-
# refute_in_delta Math::PI, (22.0 / 7.0)
-
-
1
def refute_in_delta exp, act, delta = 0.001, msg = nil
-
n = (exp - act).abs
-
msg = message(msg) {
-
"Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}"
-
}
-
refute delta >= n, msg
-
end
-
-
##
-
# For comparing Floats. Fails if +exp+ and +act+ have a relative error
-
# less than +epsilon+.
-
-
1
def refute_in_epsilon a, b, epsilon = 0.001, msg = nil
-
refute_in_delta a, b, a * epsilon, msg
-
end
-
-
##
-
# Fails if +collection+ includes +obj+.
-
-
1
def refute_includes collection, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}"
-
}
-
assert_respond_to collection, :include?
-
refute collection.include?(obj), msg
-
end
-
-
##
-
# Fails if +obj+ is an instance of +cls+.
-
-
1
def refute_instance_of cls, obj, msg = nil
-
msg = message(msg) {
-
"Expected #{mu_pp(obj)} to not be an instance of #{cls}"
-
}
-
refute obj.instance_of?(cls), msg
-
end
-
-
##
-
# Fails if +obj+ is a kind of +cls+.
-
-
1
def refute_kind_of cls, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" }
-
refute obj.kind_of?(cls), msg
-
end
-
-
##
-
# Fails if +matcher+ <tt>=~</tt> +obj+.
-
-
1
def refute_match matcher, obj, msg = nil
-
msg = message(msg) { "Expected #{mu_pp matcher} to not match #{mu_pp obj}" }
-
assert_respond_to matcher, :"=~"
-
matcher = Regexp.new Regexp.escape matcher if String === matcher
-
refute matcher =~ obj, msg
-
end
-
-
##
-
# Fails if +obj+ is nil.
-
-
1
def refute_nil obj, msg = nil
-
179
msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" }
-
179
refute obj.nil?, msg
-
end
-
-
##
-
# Fails if +o1+ is not +op+ +o2+. Eg:
-
#
-
# refute_operator 1, :>, 2 #=> pass
-
# refute_operator 1, :<, 2 #=> fail
-
-
1
def refute_operator o1, op, o2 = UNDEFINED, msg = nil
-
return refute_predicate o1, op, msg if UNDEFINED == o2
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}" }
-
refute o1.__send__(op, o2), msg
-
end
-
-
##
-
# Fails if +path+ exists.
-
-
1
def refute_path_exists path, msg = nil
-
msg = message(msg) { "Expected path '#{path}' to not exist" }
-
refute File.exist?(path), msg
-
end
-
-
##
-
# For testing with predicates.
-
#
-
# refute_predicate str, :empty?
-
#
-
# This is really meant for specs and is front-ended by refute_operator:
-
#
-
# str.wont_be :empty?
-
-
1
def refute_predicate o1, op, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
-
refute o1.__send__(op), msg
-
end
-
-
##
-
# Fails if +obj+ responds to the message +meth+.
-
-
1
def refute_respond_to obj, meth, msg = nil
-
msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" }
-
-
refute obj.respond_to?(meth), msg
-
end
-
-
##
-
# Fails if +exp+ is the same (by object identity) as +act+.
-
-
1
def refute_same exp, act, msg = nil
-
msg = message(msg) {
-
data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id]
-
"Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data
-
}
-
refute exp.equal?(act), msg
-
end
-
-
##
-
# Skips the current run. If run in verbose-mode, the skipped run
-
# gets listed at the end of the run but doesn't cause a failure
-
# exit code.
-
-
1
def skip msg = nil, bt = caller
-
msg ||= "Skipped, no message given"
-
@skip = true
-
raise Minitest::Skip, msg, bt
-
end
-
-
##
-
# Skips the current run until a given date (in the local time
-
# zone). This allows you to put some fixes on hold until a later
-
# date, but still holds you accountable and prevents you from
-
# forgetting it.
-
-
1
def skip_until y,m,d,msg
-
skip msg if Time.now < Time.local(y, m, d)
-
where = caller.first.split(/:/, 3).first(2).join ":"
-
warn "Stale skip_until %p at %s" % [msg, where]
-
end
-
-
##
-
# Was this testcase skipped? Meant for #teardown.
-
-
1
def skipped?
-
defined?(@skip) and @skip
-
end
-
end
-
end
-
begin
-
1
require "rubygems"
-
1
gem "minitest"
-
rescue Gem::LoadError
-
# do nothing
-
end
-
-
1
require "minitest"
-
1
require "minitest/spec"
-
1
require "minitest/mock"
-
1
require "minitest/hell" if ENV["MT_HELL"]
-
-
1
Minitest.autorun
-
##
-
# It's where you hide your "assertions".
-
#
-
# Please note, because of the way that expectations are implemented,
-
# all expectations (eg must_equal) are dependent upon a thread local
-
# variable +:current_spec+. If your specs rely on mixing threads into
-
# the specs themselves, you're better off using assertions or the new
-
# _(value) wrapper. For example:
-
#
-
# it "should still work in threads" do
-
# my_threaded_thingy do
-
# (1+1).must_equal 2 # bad
-
# assert_equal 2, 1+1 # good
-
# _(1 + 1).must_equal 2 # good
-
# value(1 + 1).must_equal 2 # good, also #expect
-
# end
-
# end
-
-
1
module Minitest::Expectations
-
-
##
-
# See Minitest::Assertions#assert_empty.
-
#
-
# collection.must_be_empty
-
#
-
# :method: must_be_empty
-
-
1
infect_an_assertion :assert_empty, :must_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#assert_equal
-
#
-
# a.must_equal b
-
#
-
# :method: must_equal
-
-
1
infect_an_assertion :assert_equal, :must_equal
-
-
##
-
# See Minitest::Assertions#assert_in_delta
-
#
-
# n.must_be_close_to m [, delta]
-
#
-
# :method: must_be_close_to
-
-
1
infect_an_assertion :assert_in_delta, :must_be_close_to
-
-
1
alias :must_be_within_delta :must_be_close_to # :nodoc:
-
-
##
-
# See Minitest::Assertions#assert_in_epsilon
-
#
-
# n.must_be_within_epsilon m [, epsilon]
-
#
-
# :method: must_be_within_epsilon
-
-
1
infect_an_assertion :assert_in_epsilon, :must_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#assert_includes
-
#
-
# collection.must_include obj
-
#
-
# :method: must_include
-
-
1
infect_an_assertion :assert_includes, :must_include, :reverse
-
-
##
-
# See Minitest::Assertions#assert_instance_of
-
#
-
# obj.must_be_instance_of klass
-
#
-
# :method: must_be_instance_of
-
-
1
infect_an_assertion :assert_instance_of, :must_be_instance_of
-
-
##
-
# See Minitest::Assertions#assert_kind_of
-
#
-
# obj.must_be_kind_of mod
-
#
-
# :method: must_be_kind_of
-
-
1
infect_an_assertion :assert_kind_of, :must_be_kind_of
-
-
##
-
# See Minitest::Assertions#assert_match
-
#
-
# a.must_match b
-
#
-
# :method: must_match
-
-
1
infect_an_assertion :assert_match, :must_match
-
-
##
-
# See Minitest::Assertions#assert_nil
-
#
-
# obj.must_be_nil
-
#
-
# :method: must_be_nil
-
-
1
infect_an_assertion :assert_nil, :must_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#assert_operator
-
#
-
# n.must_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# str.must_be :empty?
-
#
-
# :method: must_be
-
-
1
infect_an_assertion :assert_operator, :must_be, :reverse
-
-
##
-
# See Minitest::Assertions#assert_output
-
#
-
# proc { ... }.must_output out_or_nil [, err]
-
#
-
# :method: must_output
-
-
1
infect_an_assertion :assert_output, :must_output, :block
-
-
##
-
# See Minitest::Assertions#assert_raises
-
#
-
# proc { ... }.must_raise exception
-
#
-
# :method: must_raise
-
-
1
infect_an_assertion :assert_raises, :must_raise, :block
-
-
##
-
# See Minitest::Assertions#assert_respond_to
-
#
-
# obj.must_respond_to msg
-
#
-
# :method: must_respond_to
-
-
1
infect_an_assertion :assert_respond_to, :must_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#assert_same
-
#
-
# a.must_be_same_as b
-
#
-
# :method: must_be_same_as
-
-
1
infect_an_assertion :assert_same, :must_be_same_as
-
-
##
-
# See Minitest::Assertions#assert_silent
-
#
-
# proc { ... }.must_be_silent
-
#
-
# :method: must_be_silent
-
-
1
infect_an_assertion :assert_silent, :must_be_silent, :block
-
-
##
-
# See Minitest::Assertions#assert_throws
-
#
-
# proc { ... }.must_throw sym
-
#
-
# :method: must_throw
-
-
1
infect_an_assertion :assert_throws, :must_throw, :block
-
-
##
-
# See Minitest::Assertions#assert_path_exists
-
#
-
# _(some_path).path_must_exist
-
#
-
# :method: path_must_exist
-
-
1
infect_an_assertion :assert_path_exists, :path_must_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_path_exists
-
#
-
# _(some_path).path_wont_exist
-
#
-
# :method: path_wont_exist
-
-
1
infect_an_assertion :refute_path_exists, :path_wont_exist, :unary
-
-
##
-
# See Minitest::Assertions#refute_empty
-
#
-
# collection.wont_be_empty
-
#
-
# :method: wont_be_empty
-
-
1
infect_an_assertion :refute_empty, :wont_be_empty, :unary
-
-
##
-
# See Minitest::Assertions#refute_equal
-
#
-
# a.wont_equal b
-
#
-
# :method: wont_equal
-
-
1
infect_an_assertion :refute_equal, :wont_equal
-
-
##
-
# See Minitest::Assertions#refute_in_delta
-
#
-
# n.wont_be_close_to m [, delta]
-
#
-
# :method: wont_be_close_to
-
-
1
infect_an_assertion :refute_in_delta, :wont_be_close_to
-
-
1
alias :wont_be_within_delta :wont_be_close_to # :nodoc:
-
-
##
-
# See Minitest::Assertions#refute_in_epsilon
-
#
-
# n.wont_be_within_epsilon m [, epsilon]
-
#
-
# :method: wont_be_within_epsilon
-
-
1
infect_an_assertion :refute_in_epsilon, :wont_be_within_epsilon
-
-
##
-
# See Minitest::Assertions#refute_includes
-
#
-
# collection.wont_include obj
-
#
-
# :method: wont_include
-
-
1
infect_an_assertion :refute_includes, :wont_include, :reverse
-
-
##
-
# See Minitest::Assertions#refute_instance_of
-
#
-
# obj.wont_be_instance_of klass
-
#
-
# :method: wont_be_instance_of
-
-
1
infect_an_assertion :refute_instance_of, :wont_be_instance_of
-
-
##
-
# See Minitest::Assertions#refute_kind_of
-
#
-
# obj.wont_be_kind_of mod
-
#
-
# :method: wont_be_kind_of
-
-
1
infect_an_assertion :refute_kind_of, :wont_be_kind_of
-
-
##
-
# See Minitest::Assertions#refute_match
-
#
-
# a.wont_match b
-
#
-
# :method: wont_match
-
-
1
infect_an_assertion :refute_match, :wont_match
-
-
##
-
# See Minitest::Assertions#refute_nil
-
#
-
# obj.wont_be_nil
-
#
-
# :method: wont_be_nil
-
-
1
infect_an_assertion :refute_nil, :wont_be_nil, :unary
-
-
##
-
# See Minitest::Assertions#refute_operator
-
#
-
# n.wont_be :<=, 42
-
#
-
# This can also do predicates:
-
#
-
# str.wont_be :empty?
-
#
-
# :method: wont_be
-
-
1
infect_an_assertion :refute_operator, :wont_be, :reverse
-
-
##
-
# See Minitest::Assertions#refute_respond_to
-
#
-
# obj.wont_respond_to msg
-
#
-
# :method: wont_respond_to
-
-
1
infect_an_assertion :refute_respond_to, :wont_respond_to, :reverse
-
-
##
-
# See Minitest::Assertions#refute_same
-
#
-
# a.wont_be_same_as b
-
#
-
# :method: wont_be_same_as
-
-
1
infect_an_assertion :refute_same, :wont_be_same_as
-
end
-
1
class MockExpectationError < StandardError; end # :nodoc:
-
-
1
module Minitest # :nodoc:
-
-
##
-
# A simple and clean mock object framework.
-
#
-
# All mock objects are an instance of Mock
-
-
1
class Mock
-
1
alias :__respond_to? :respond_to?
-
-
overridden_methods = %w[
-
1
===
-
class
-
inspect
-
instance_eval
-
instance_variables
-
object_id
-
public_send
-
respond_to_missing?
-
send
-
to_s
-
]
-
-
1
instance_methods.each do |m|
-
92
undef_method m unless overridden_methods.include?(m.to_s) || m =~ /^__/
-
end
-
-
1
overridden_methods.map(&:to_sym).each do |method_id|
-
10
define_method method_id do |*args, &b|
-
if @expected_calls.key? method_id then
-
method_missing(method_id, *args, &b)
-
else
-
super(*args, &b)
-
end
-
end
-
end
-
-
1
def initialize delegator = nil # :nodoc:
-
@delegator = delegator
-
@expected_calls = Hash.new { |calls, name| calls[name] = [] }
-
@actual_calls = Hash.new { |calls, name| calls[name] = [] }
-
end
-
-
##
-
# Expect that method +name+ is called, optionally with +args+ or a
-
# +blk+, and returns +retval+.
-
#
-
# @mock.expect(:meaning_of_life, 42)
-
# @mock.meaning_of_life # => 42
-
#
-
# @mock.expect(:do_something_with, true, [some_obj, true])
-
# @mock.do_something_with(some_obj, true) # => true
-
#
-
# @mock.expect(:do_something_else, true) do |a1, a2|
-
# a1 == "buggs" && a2 == :bunny
-
# end
-
#
-
# +args+ is compared to the expected args using case equality (ie, the
-
# '===' operator), allowing for less specific expectations.
-
#
-
# @mock.expect(:uses_any_string, true, [String])
-
# @mock.uses_any_string("foo") # => true
-
# @mock.verify # => true
-
#
-
# @mock.expect(:uses_one_string, true, ["foo"])
-
# @mock.uses_one_string("bar") # => raises MockExpectationError
-
#
-
# If a method will be called multiple times, specify a new expect for each one.
-
# They will be used in the order you define them.
-
#
-
# @mock.expect(:ordinal_increment, 'first')
-
# @mock.expect(:ordinal_increment, 'second')
-
#
-
# @mock.ordinal_increment # => 'first'
-
# @mock.ordinal_increment # => 'second'
-
# @mock.ordinal_increment # => raises MockExpectationError "No more expects available for :ordinal_increment"
-
#
-
-
1
def expect name, retval, args = [], &blk
-
name = name.to_sym
-
-
if block_given?
-
raise ArgumentError, "args ignored when block given" unless args.empty?
-
@expected_calls[name] << { :retval => retval, :block => blk }
-
else
-
raise ArgumentError, "args must be an array" unless Array === args
-
@expected_calls[name] << { :retval => retval, :args => args }
-
end
-
self
-
end
-
-
1
def __call name, data # :nodoc:
-
case data
-
when Hash then
-
"#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
-
else
-
data.map { |d| __call name, d }.join ", "
-
end
-
end
-
-
##
-
# Verify that all methods were called as expected. Raises
-
# +MockExpectationError+ if the mock object was not called as
-
# expected.
-
-
1
def verify
-
@expected_calls.each do |name, expected|
-
actual = @actual_calls.fetch(name, nil)
-
raise MockExpectationError, "expected #{__call name, expected[0]}" unless actual
-
raise MockExpectationError, "expected #{__call name, expected[actual.size]}, got [#{__call name, actual}]" if
-
actual.size < expected.size
-
end
-
true
-
end
-
-
1
def method_missing sym, *args, &block # :nodoc:
-
unless @expected_calls.key?(sym) then
-
if @delegator && @delegator.respond_to?(sym)
-
return @delegator.public_send(sym, *args, &block)
-
else
-
raise NoMethodError, "unmocked method %p, expected one of %p" %
-
[sym, @expected_calls.keys.sort_by(&:to_s)]
-
end
-
end
-
-
index = @actual_calls[sym].length
-
expected_call = @expected_calls[sym][index]
-
-
unless expected_call then
-
raise MockExpectationError, "No more expects available for %p: %p" %
-
[sym, args]
-
end
-
-
expected_args, retval, val_block =
-
expected_call.values_at(:args, :retval, :block)
-
-
if val_block then
-
# keep "verify" happy
-
@actual_calls[sym] << expected_call
-
-
raise MockExpectationError, "mocked method %p failed block w/ %p" %
-
[sym, args] unless val_block.call(*args, &block)
-
-
return retval
-
end
-
-
if expected_args.size != args.size then
-
raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
-
[sym, expected_args.size, args.size]
-
end
-
-
zipped_args = expected_args.zip(args)
-
fully_matched = zipped_args.all? { |mod, a|
-
mod === a or mod == a
-
}
-
-
unless fully_matched then
-
raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
-
[sym, args]
-
end
-
-
@actual_calls[sym] << {
-
:retval => retval,
-
:args => zipped_args.map! { |mod, a| mod === a ? mod : a },
-
}
-
-
retval
-
end
-
-
1
def respond_to? sym, include_private = false # :nodoc:
-
return true if @expected_calls.key? sym.to_sym
-
return true if @delegator && @delegator.respond_to?(sym, include_private)
-
__respond_to?(sym, include_private)
-
end
-
end
-
end
-
-
1
module Minitest::Assertions
-
##
-
# Assert that the mock verifies correctly.
-
-
1
def assert_mock mock
-
assert mock.verify
-
end
-
end
-
-
##
-
# Object extensions for Minitest::Mock.
-
-
1
class Object
-
-
##
-
# Add a temporary stubbed method replacing +name+ for the duration
-
# of the +block+. If +val_or_callable+ responds to #call, then it
-
# returns the result of calling it, otherwise returns the value
-
# as-is. If stubbed method yields a block, +block_args+ will be
-
# passed along. Cleans up the stub at the end of the +block+. The
-
# method +name+ must exist before stubbing.
-
#
-
# def test_stale_eh
-
# obj_under_test = Something.new
-
# refute obj_under_test.stale?
-
#
-
# Time.stub :now, Time.at(0) do
-
# assert obj_under_test.stale?
-
# end
-
# end
-
#
-
-
1
def stub name, val_or_callable, *block_args
-
new_name = "__minitest_stub__#{name}"
-
-
metaclass = class << self; self; end
-
-
if respond_to? name and not methods.map(&:to_s).include? name.to_s then
-
metaclass.send :define_method, name do |*args|
-
super(*args)
-
end
-
end
-
-
metaclass.send :alias_method, new_name, name
-
-
metaclass.send :define_method, name do |*args, &blk|
-
if val_or_callable.respond_to? :call then
-
val_or_callable.call(*args, &blk)
-
else
-
blk.call(*block_args) if blk
-
val_or_callable
-
end
-
end
-
-
yield self
-
ensure
-
metaclass.send :undef_method, name
-
metaclass.send :alias_method, name, new_name
-
metaclass.send :undef_method, new_name
-
end
-
end
-
1
module Minitest
-
1
module Parallel #:nodoc:
-
-
##
-
# The engine used to run multiple tests in parallel.
-
-
1
class Executor
-
-
##
-
# The size of the pool of workers.
-
-
1
attr_reader :size
-
-
##
-
# Create a parallel test executor of with +size+ workers.
-
-
1
def initialize size
-
1
@size = size
-
1
@queue = Queue.new
-
1
@pool = nil
-
end
-
-
##
-
# Start the executor
-
-
1
def start
-
1
@pool = size.times.map {
-
2
Thread.new(@queue) do |queue|
-
2
Thread.current.abort_on_exception = true
-
4
while (job = queue.pop)
-
klass, method, reporter = job
-
reporter.synchronize { reporter.prerecord klass, method }
-
result = Minitest.run_one_method klass, method
-
reporter.synchronize { reporter.record result }
-
end
-
end
-
}
-
end
-
-
##
-
# Add a job to the queue
-
-
1
def << work; @queue << work; end
-
-
##
-
# Shuts down the pool of workers by signalling them to quit and
-
# waiting for them all to finish what they're currently working
-
# on.
-
-
1
def shutdown
-
3
size.times { @queue << nil }
-
1
@pool.each(&:join)
-
end
-
end
-
-
1
module Test # :nodoc:
-
1
def _synchronize; Minitest::Test.io_lock.synchronize { yield }; end # :nodoc:
-
-
1
module ClassMethods # :nodoc:
-
1
def run_one_method klass, method_name, reporter
-
Minitest.parallel_executor << [klass, method_name, reporter]
-
end
-
-
1
def test_order
-
:parallel
-
end
-
end
-
end
-
end
-
end
-
1
require "minitest"
-
-
1
module Minitest
-
1
def self.plugin_pride_options opts, _options # :nodoc:
-
1
opts.on "-p", "--pride", "Pride. Show your testing pride!" do
-
PrideIO.pride!
-
end
-
end
-
-
1
def self.plugin_pride_init options # :nodoc:
-
1
if PrideIO.pride? then
-
klass = ENV["TERM"] =~ /^xterm|-256color$/ ? PrideLOL : PrideIO
-
io = klass.new options[:io]
-
-
self.reporter.reporters.grep(Minitest::Reporter).each do |rep|
-
rep.io = io if rep.io.tty?
-
end
-
end
-
end
-
-
##
-
# Show your testing pride!
-
-
1
class PrideIO
-
##
-
# Activate the pride plugin. Called from both -p option and minitest/pride
-
-
1
def self.pride!
-
@pride = true
-
end
-
-
##
-
# Are we showing our testing pride?
-
-
1
def self.pride?
-
1
@pride ||= false
-
end
-
-
# Start an escape sequence
-
1
ESC = "\e["
-
-
# End the escape sequence
-
1
NND = "#{ESC}0m"
-
-
# The IO we're going to pipe through.
-
1
attr_reader :io
-
-
1
def initialize io # :nodoc:
-
@io = io
-
# stolen from /System/Library/Perl/5.10.0/Term/ANSIColor.pm
-
# also reference http://en.wikipedia.org/wiki/ANSI_escape_code
-
@colors ||= (31..36).to_a
-
@size = @colors.size
-
@index = 0
-
end
-
-
##
-
# Wrap print to colorize the output.
-
-
1
def print o
-
case o
-
when "." then
-
io.print pride o
-
when "E", "F" then
-
io.print "#{ESC}41m#{ESC}37m#{o}#{NND}"
-
when "S" then
-
io.print pride o
-
else
-
io.print o
-
end
-
end
-
-
1
def puts *o # :nodoc:
-
o.map! { |s|
-
s.to_s.sub(/Finished/) {
-
@index = 0
-
"Fabulous run".split(//).map { |c|
-
pride(c)
-
}.join
-
}
-
}
-
-
io.puts(*o)
-
end
-
-
##
-
# Color a string.
-
-
1
def pride string
-
string = "*" if string == "."
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}#{c}m#{string}#{NND}"
-
end
-
-
1
def method_missing msg, *args # :nodoc:
-
io.send(msg, *args)
-
end
-
end
-
-
##
-
# If you thought the PrideIO was colorful...
-
#
-
# (Inspired by lolcat, but with clean math)
-
-
1
class PrideLOL < PrideIO
-
1
PI_3 = Math::PI / 3 # :nodoc:
-
-
1
def initialize io # :nodoc:
-
# walk red, green, and blue around a circle separated by equal thirds.
-
#
-
# To visualize, type this into wolfram-alpha:
-
#
-
# plot (3*sin(x)+3), (3*sin(x+2*pi/3)+3), (3*sin(x+4*pi/3)+3)
-
-
# 6 has wide pretty gradients. 3 == lolcat, about half the width
-
@colors = (0...(6 * 7)).map { |n|
-
n *= 1.0 / 6
-
r = (3 * Math.sin(n ) + 3).to_i
-
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
-
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
-
-
# Then we take rgb and encode them in a single number using base 6.
-
# For some mysterious reason, we add 16... to clear the bottom 4 bits?
-
# Yes... they're ugly.
-
-
36 * r + 6 * g + b + 16
-
}
-
-
super
-
end
-
-
##
-
# Make the string even more colorful. Damnit.
-
-
1
def pride string
-
c = @colors[@index % @size]
-
@index += 1
-
"#{ESC}38;5;#{c}m#{string}#{NND}"
-
end
-
end
-
end
-
1
require "minitest/test"
-
-
1
class Module # :nodoc:
-
1
def infect_an_assertion meth, new_name, dont_flip = false # :nodoc:
-
30
block = dont_flip == :block
-
30
dont_flip = false if block
-
-
# warn "%-22p -> %p %p" % [meth, new_name, dont_flip]
-
30
self.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
where = Minitest.filter_backtrace(caller).first
-
where = where.split(/:in /, 2).first # clean up noise
-
warn "DEPRECATED: global use of #{new_name} from #\{where}. Use _(obj).#{new_name} instead. This will fail in Minitest 6."
-
Minitest::Expectation.new(self, Minitest::Spec.current).#{new_name}(*args)
-
end
-
EOM
-
-
30
Minitest::Expectation.class_eval <<-EOM, __FILE__, __LINE__ + 1
-
def #{new_name} *args
-
raise "Calling ##{new_name} outside of test." unless ctx
-
case
-
when #{!!dont_flip} then
-
ctx.#{meth}(target, *args)
-
when #{block} && Proc === target then
-
ctx.#{meth}(*args, &target)
-
else
-
ctx.#{meth}(args.first, target, *args[1..-1])
-
end
-
end
-
EOM
-
end
-
end
-
-
1
Minitest::Expectation = Struct.new :target, :ctx # :nodoc:
-
-
##
-
# Kernel extensions for minitest
-
-
1
module Kernel
-
##
-
# Describe a series of expectations for a given target +desc+.
-
#
-
# Defines a test class subclassing from either Minitest::Spec or
-
# from the surrounding describe's class. The surrounding class may
-
# subclass Minitest::Spec manually in order to easily share code:
-
#
-
# class MySpec < Minitest::Spec
-
# # ... shared code ...
-
# end
-
#
-
# class TestStuff < MySpec
-
# it "does stuff" do
-
# # shared code available here
-
# end
-
# describe "inner stuff" do
-
# it "still does stuff" do
-
# # ...and here
-
# end
-
# end
-
# end
-
#
-
# For more information on getting started with writing specs, see:
-
#
-
# http://www.rubyinside.com/a-minitestspec-tutorial-elegant-spec-style-testing-that-comes-with-ruby-5354.html
-
#
-
# For some suggestions on how to improve your specs, try:
-
#
-
# http://betterspecs.org
-
#
-
# but do note that several items there are debatable or specific to
-
# rspec.
-
#
-
# For more information about expectations, see Minitest::Expectations.
-
-
1
def describe desc, *additional_desc, &block # :doc:
-
stack = Minitest::Spec.describe_stack
-
name = [stack.last, desc, *additional_desc].compact.join("::")
-
sclas = stack.last || if Class === self && kind_of?(Minitest::Spec::DSL) then
-
self
-
else
-
Minitest::Spec.spec_type desc, *additional_desc
-
end
-
-
cls = sclas.create name, desc
-
-
stack.push cls
-
cls.class_eval(&block)
-
stack.pop
-
cls
-
end
-
1
private :describe
-
end
-
-
##
-
# Minitest::Spec -- The faster, better, less-magical spec framework!
-
#
-
# For a list of expectations, see Minitest::Expectations.
-
-
1
class Minitest::Spec < Minitest::Test
-
-
1
def self.current # :nodoc:
-
Thread.current[:current_spec]
-
end
-
-
1
def initialize name # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
-
##
-
# Oh look! A Minitest::Spec::DSL module! Eat your heart out DHH.
-
-
1
module DSL
-
##
-
# Contains pairs of matchers and Spec classes to be used to
-
# calculate the superclass of a top-level describe. This allows for
-
# automatically customizable spec types.
-
#
-
# See: register_spec_type and spec_type
-
-
1
TYPES = [[//, Minitest::Spec]]
-
-
##
-
# Register a new type of spec that matches the spec's description.
-
# This method can take either a Regexp and a spec class or a spec
-
# class and a block that takes the description and returns true if
-
# it matches.
-
#
-
# Eg:
-
#
-
# register_spec_type(/Controller$/, Minitest::Spec::Rails)
-
#
-
# or:
-
#
-
# register_spec_type(Minitest::Spec::RailsModel) do |desc|
-
# desc.superclass == ActiveRecord::Base
-
# end
-
-
1
def register_spec_type *args, &block
-
if block then
-
matcher, klass = block, args.first
-
else
-
matcher, klass = *args
-
end
-
TYPES.unshift [matcher, klass]
-
end
-
-
##
-
# Figure out the spec class to use based on a spec's description. Eg:
-
#
-
# spec_type("BlahController") # => Minitest::Spec::Rails
-
-
1
def spec_type desc, *additional
-
TYPES.find { |matcher, _klass|
-
if matcher.respond_to? :call then
-
matcher.call desc, *additional
-
else
-
matcher === desc.to_s
-
end
-
}.last
-
end
-
-
1
def describe_stack # :nodoc:
-
Thread.current[:describe_stack] ||= []
-
end
-
-
1
def children # :nodoc:
-
@children ||= []
-
end
-
-
1
def nuke_test_methods! # :nodoc:
-
self.public_instance_methods.grep(/^test_/).each do |name|
-
self.send :undef_method, name
-
end
-
end
-
-
##
-
# Define a 'before' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#setup.
-
-
1
def before _type = nil, &block
-
define_method :setup do
-
super()
-
self.instance_eval(&block)
-
end
-
end
-
-
##
-
# Define an 'after' action. Inherits the way normal methods should.
-
#
-
# NOTE: +type+ is ignored and is only there to make porting easier.
-
#
-
# Equivalent to Minitest::Test#teardown.
-
-
1
def after _type = nil, &block
-
define_method :teardown do
-
self.instance_eval(&block)
-
super()
-
end
-
end
-
-
##
-
# Define an expectation with name +desc+. Name gets morphed to a
-
# proper test method name. For some freakish reason, people who
-
# write specs don't like class inheritance, so this goes way out of
-
# its way to make sure that expectations aren't inherited.
-
#
-
# This is also aliased to #specify and doesn't require a +desc+ arg.
-
#
-
# Hint: If you _do_ want inheritance, use minitest/test. You can mix
-
# and match between assertions and expectations as much as you want.
-
-
1
def it desc = "anonymous", &block
-
block ||= proc { skip "(no tests defined)" }
-
-
@specs ||= 0
-
@specs += 1
-
-
name = "test_%04d_%s" % [ @specs, desc ]
-
-
undef_klasses = self.children.reject { |c| c.public_method_defined? name }
-
-
define_method name, &block
-
-
undef_klasses.each do |undef_klass|
-
undef_klass.send :undef_method, name
-
end
-
-
name
-
end
-
-
##
-
# Essentially, define an accessor for +name+ with +block+.
-
#
-
# Why use let instead of def? I honestly don't know.
-
-
1
def let name, &block
-
name = name.to_s
-
pre, post = "let '#{name}' cannot ", ". Please use another name."
-
methods = Minitest::Spec.instance_methods.map(&:to_s) - %w[subject]
-
raise ArgumentError, "#{pre}begin with 'test'#{post}" if
-
name =~ /\Atest/
-
raise ArgumentError, "#{pre}override a method in Minitest::Spec#{post}" if
-
methods.include? name
-
-
define_method name do
-
@_memoized ||= {}
-
@_memoized.fetch(name) { |k| @_memoized[k] = instance_eval(&block) }
-
end
-
end
-
-
##
-
# Another lazy man's accessor generator. Made even more lazy by
-
# setting the name for you to +subject+.
-
-
1
def subject &block
-
let :subject, &block
-
end
-
-
1
def create name, desc # :nodoc:
-
cls = Class.new(self) do
-
@name = name
-
@desc = desc
-
-
nuke_test_methods!
-
end
-
-
children << cls
-
-
cls
-
end
-
-
1
def name # :nodoc:
-
defined?(@name) ? @name : super
-
end
-
-
1
def to_s # :nodoc:
-
name # Can't alias due to 1.8.7, not sure why
-
end
-
-
1
attr_reader :desc # :nodoc:
-
1
alias :specify :it
-
-
##
-
# Rdoc... why are you so dumb?
-
-
1
module InstanceMethods
-
##
-
# Takes a value or a block and returns a value monad that has
-
# all of Expectations methods available to it.
-
#
-
# _(1 + 1).must_equal 2
-
#
-
# And for blocks:
-
#
-
# _ { 1 + "1" }.must_raise TypeError
-
#
-
# This method of expectation-based testing is preferable to
-
# straight-expectation methods (on Object) because it stores its
-
# test context, bypassing our hacky use of thread-local variables.
-
#
-
# NOTE: At some point, the methods on Object will be deprecated
-
# and then removed.
-
#
-
# It is also aliased to #value and #expect for your aesthetic
-
# pleasure:
-
#
-
# _(1 + 1).must_equal 2
-
# value(1 + 1).must_equal 2
-
# expect(1 + 1).must_equal 2
-
-
1
def _ value = nil, &block
-
Minitest::Expectation.new block || value, self
-
end
-
-
1
alias value _
-
1
alias expect _
-
-
1
def before_setup # :nodoc:
-
super
-
Thread.current[:current_spec] = self
-
end
-
end
-
-
1
def self.extended obj # :nodoc:
-
1
obj.send :include, InstanceMethods
-
end
-
end
-
-
1
extend DSL
-
-
1
TYPES = DSL::TYPES # :nodoc:
-
end
-
-
1
require "minitest/expectations"
-
-
1
class Object # :nodoc:
-
1
include Minitest::Expectations unless ENV["MT_NO_EXPECTATIONS"]
-
end
-
1
require "minitest" unless defined? Minitest::Runnable
-
-
1
module Minitest
-
##
-
# Subclass Test to create your own tests. Typically you'll want a
-
# Test subclass per implementation class.
-
#
-
# See Minitest::Assertions
-
-
1
class Test < Runnable
-
1
require "minitest/assertions"
-
1
include Minitest::Assertions
-
1
include Minitest::Reportable
-
-
1
def class_name # :nodoc:
-
self.class.name # for Minitest::Reportable
-
end
-
-
1
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, SystemExit] # :nodoc:
-
-
# :stopdoc:
-
2
class << self; attr_accessor :io_lock; end
-
1
self.io_lock = Mutex.new
-
# :startdoc:
-
-
##
-
# Call this at the top of your tests when you absolutely
-
# positively need to have ordered tests. In doing so, you're
-
# admitting that you suck and your tests are weak.
-
-
1
def self.i_suck_and_my_tests_are_order_dependent!
-
class << self
-
undef_method :test_order if method_defined? :test_order
-
define_method :test_order do :alpha end
-
end
-
end
-
-
##
-
# Make diffs for this Test use #pretty_inspect so that diff
-
# in assert_equal can have more details. NOTE: this is much slower
-
# than the regular inspect but much more usable for complex
-
# objects.
-
-
1
def self.make_my_diffs_pretty!
-
require "pp"
-
-
define_method :mu_pp, &:pretty_inspect
-
end
-
-
##
-
# Call this at the top of your tests when you want to run your
-
# tests in parallel. In doing so, you're admitting that you rule
-
# and your tests are awesome.
-
-
1
def self.parallelize_me!
-
include Minitest::Parallel::Test
-
extend Minitest::Parallel::Test::ClassMethods
-
end
-
-
##
-
# Returns all instance methods starting with "test_". Based on
-
# #test_order, the methods are either sorted, randomized
-
# (default), or run in parallel.
-
-
1
def self.runnable_methods
-
51
methods = methods_matching(/^test_/)
-
-
51
case self.test_order
-
when :random, :parallel then
-
51
max = methods.size
-
275
methods.sort.sort_by { rand max }
-
when :alpha, :sorted then
-
methods.sort
-
else
-
raise "Unknown test_order: #{self.test_order.inspect}"
-
end
-
end
-
-
##
-
# Defines the order to run tests (:random by default). Override
-
# this or use a convenience method to change it for your tests.
-
-
1
def self.test_order
-
74
:random
-
end
-
-
1
TEARDOWN_METHODS = %w[ before_teardown teardown after_teardown ] # :nodoc:
-
-
##
-
# Runs a single test with setup/teardown hooks.
-
-
1
def run
-
112
with_info_handler do
-
112
time_it do
-
112
capture_exceptions do
-
112
before_setup; setup; after_setup
-
-
112
self.send self.name
-
end
-
-
112
TEARDOWN_METHODS.each do |hook|
-
336
capture_exceptions do
-
336
self.send hook
-
end
-
end
-
end
-
end
-
-
112
Result.from self # per contract
-
end
-
-
##
-
# Provides before/after hooks for setup and teardown. These are
-
# meant for library writers, NOT for regular test authors. See
-
# #before_setup for an example.
-
-
1
module LifecycleHooks
-
-
##
-
# Runs before every test, before setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# As a simplistic example:
-
#
-
# module MyMinitestPlugin
-
# def before_setup
-
# super
-
# # ... stuff to do before setup is run
-
# end
-
#
-
# def after_setup
-
# # ... stuff to do after setup is run
-
# super
-
# end
-
#
-
# def before_teardown
-
# super
-
# # ... stuff to do before teardown is run
-
# end
-
#
-
# def after_teardown
-
# # ... stuff to do after teardown is run
-
# super
-
# end
-
# end
-
#
-
# class MiniTest::Test
-
# include MyMinitestPlugin
-
# end
-
-
1
def before_setup; end
-
-
##
-
# Runs before every test. Use this to set up before each test
-
# run.
-
-
1
def setup; end
-
-
##
-
# Runs before every test, after setup. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_setup; end
-
-
##
-
# Runs after every test, before teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def before_teardown; end
-
-
##
-
# Runs after every test. Use this to clean up after each test
-
# run.
-
-
1
def teardown; end
-
-
##
-
# Runs after every test, after teardown. This hook is meant for
-
# libraries to extend minitest. It is not meant to be used by
-
# test developers.
-
#
-
# See #before_setup for an example.
-
-
1
def after_teardown; end
-
end # LifecycleHooks
-
-
1
def capture_exceptions # :nodoc:
-
448
yield
-
rescue *PASSTHROUGH_EXCEPTIONS
-
raise
-
rescue Assertion => e
-
self.failures << e
-
rescue Exception => e
-
self.failures << UnexpectedError.new(e)
-
end
-
-
1
def with_info_handler &block # :nodoc:
-
112
t0 = Minitest.clock_time
-
-
112
handler = lambda do
-
warn "\nCurrent: %s#%s %.2fs" % [self.class, self.name, Minitest.clock_time - t0]
-
end
-
-
112
self.class.on_signal ::Minitest.info_signal, handler, &block
-
end
-
-
1
include LifecycleHooks
-
1
include Guard
-
1
extend Guard
-
end # Test
-
end
-
-
1
require "minitest/unit" unless defined?(MiniTest) # compatibility layer only
-
# :stopdoc:
-
-
1
unless defined?(Minitest) then
-
# all of this crap is just to avoid circular requires and is only
-
# needed if a user requires "minitest/unit" directly instead of
-
# "minitest/autorun", so we also warn
-
-
from = caller.reject { |s| s =~ /rubygems/ }.join("\n ")
-
warn "Warning: you should require 'minitest/autorun' instead."
-
warn %(Warning: or add 'gem "minitest"' before 'require "minitest/autorun"')
-
warn "From:\n #{from}"
-
-
module Minitest; end
-
MiniTest = Minitest # prevents minitest.rb from requiring back to us
-
require "minitest"
-
end
-
-
1
MiniTest = Minitest unless defined?(MiniTest)
-
-
1
module Minitest
-
1
class Unit
-
1
VERSION = Minitest::VERSION
-
1
class TestCase < Minitest::Test
-
1
def self.inherited klass # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit::TestCase is now Minitest::Test. From #{from}"
-
super
-
end
-
end
-
-
1
def self.autorun # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.autorun is now Minitest.autorun. From #{from}"
-
Minitest.autorun
-
end
-
-
1
def self.after_tests &b # :nodoc:
-
from = caller.first
-
warn "MiniTest::Unit.after_tests is now Minitest.after_run. From #{from}"
-
Minitest.after_run(&b)
-
end
-
end
-
end
-
-
# :startdoc:
-
# Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen>
-
#
-
# Rack is freely distributable under the terms of an MIT-style license.
-
# See COPYING or http://www.opensource.org/licenses/mit-license.php.
-
-
# The Rack main module, serving as a namespace for all core Rack
-
# modules and classes.
-
#
-
# All modules meant for use in your application are <tt>autoload</tt>ed here,
-
# so it should be enough just to <tt>require 'rack'</tt> in your code.
-
-
1
module Rack
-
# The Rack protocol version number implemented.
-
1
VERSION = [1,3]
-
-
# Return the Rack protocol version as a dotted string.
-
1
def self.version
-
VERSION.join(".")
-
end
-
-
1
RELEASE = "2.0.8"
-
-
# Return the Rack release as a dotted string.
-
1
def self.release
-
RELEASE
-
end
-
-
1
HTTP_HOST = 'HTTP_HOST'.freeze
-
1
HTTP_VERSION = 'HTTP_VERSION'.freeze
-
1
HTTPS = 'HTTPS'.freeze
-
1
PATH_INFO = 'PATH_INFO'.freeze
-
1
REQUEST_METHOD = 'REQUEST_METHOD'.freeze
-
1
REQUEST_PATH = 'REQUEST_PATH'.freeze
-
1
SCRIPT_NAME = 'SCRIPT_NAME'.freeze
-
1
QUERY_STRING = 'QUERY_STRING'.freeze
-
1
SERVER_PROTOCOL = 'SERVER_PROTOCOL'.freeze
-
1
SERVER_NAME = 'SERVER_NAME'.freeze
-
1
SERVER_ADDR = 'SERVER_ADDR'.freeze
-
1
SERVER_PORT = 'SERVER_PORT'.freeze
-
1
CACHE_CONTROL = 'Cache-Control'.freeze
-
1
CONTENT_LENGTH = 'Content-Length'.freeze
-
1
CONTENT_TYPE = 'Content-Type'.freeze
-
1
SET_COOKIE = 'Set-Cookie'.freeze
-
1
TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
-
1
HTTP_COOKIE = 'HTTP_COOKIE'.freeze
-
1
ETAG = 'ETag'.freeze
-
-
# HTTP method verbs
-
1
GET = 'GET'.freeze
-
1
POST = 'POST'.freeze
-
1
PUT = 'PUT'.freeze
-
1
PATCH = 'PATCH'.freeze
-
1
DELETE = 'DELETE'.freeze
-
1
HEAD = 'HEAD'.freeze
-
1
OPTIONS = 'OPTIONS'.freeze
-
1
LINK = 'LINK'.freeze
-
1
UNLINK = 'UNLINK'.freeze
-
1
TRACE = 'TRACE'.freeze
-
-
# Rack environment variables
-
1
RACK_VERSION = 'rack.version'.freeze
-
1
RACK_TEMPFILES = 'rack.tempfiles'.freeze
-
1
RACK_ERRORS = 'rack.errors'.freeze
-
1
RACK_LOGGER = 'rack.logger'.freeze
-
1
RACK_INPUT = 'rack.input'.freeze
-
1
RACK_SESSION = 'rack.session'.freeze
-
1
RACK_SESSION_OPTIONS = 'rack.session.options'.freeze
-
1
RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail'.freeze
-
1
RACK_MULTITHREAD = 'rack.multithread'.freeze
-
1
RACK_MULTIPROCESS = 'rack.multiprocess'.freeze
-
1
RACK_RUNONCE = 'rack.run_once'.freeze
-
1
RACK_URL_SCHEME = 'rack.url_scheme'.freeze
-
1
RACK_HIJACK = 'rack.hijack'.freeze
-
1
RACK_IS_HIJACK = 'rack.hijack?'.freeze
-
1
RACK_HIJACK_IO = 'rack.hijack_io'.freeze
-
1
RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'.freeze
-
1
RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'.freeze
-
1
RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'.freeze
-
1
RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'.freeze
-
1
RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'.freeze
-
1
RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'.freeze
-
1
RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'.freeze
-
1
RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'.freeze
-
1
RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'.freeze
-
1
RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'.freeze
-
1
RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'.freeze
-
1
RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'.freeze
-
-
1
autoload :Builder, "rack/builder"
-
1
autoload :BodyProxy, "rack/body_proxy"
-
1
autoload :Cascade, "rack/cascade"
-
1
autoload :Chunked, "rack/chunked"
-
1
autoload :CommonLogger, "rack/common_logger"
-
1
autoload :ConditionalGet, "rack/conditional_get"
-
1
autoload :Config, "rack/config"
-
1
autoload :ContentLength, "rack/content_length"
-
1
autoload :ContentType, "rack/content_type"
-
1
autoload :ETag, "rack/etag"
-
1
autoload :File, "rack/file"
-
1
autoload :Deflater, "rack/deflater"
-
1
autoload :Directory, "rack/directory"
-
1
autoload :ForwardRequest, "rack/recursive"
-
1
autoload :Handler, "rack/handler"
-
1
autoload :Head, "rack/head"
-
1
autoload :Lint, "rack/lint"
-
1
autoload :Lock, "rack/lock"
-
1
autoload :Logger, "rack/logger"
-
1
autoload :MethodOverride, "rack/method_override"
-
1
autoload :Mime, "rack/mime"
-
1
autoload :NullLogger, "rack/null_logger"
-
1
autoload :Recursive, "rack/recursive"
-
1
autoload :Reloader, "rack/reloader"
-
1
autoload :Runtime, "rack/runtime"
-
1
autoload :Sendfile, "rack/sendfile"
-
1
autoload :Server, "rack/server"
-
1
autoload :ShowExceptions, "rack/show_exceptions"
-
1
autoload :ShowStatus, "rack/show_status"
-
1
autoload :Static, "rack/static"
-
1
autoload :TempfileReaper, "rack/tempfile_reaper"
-
1
autoload :URLMap, "rack/urlmap"
-
1
autoload :Utils, "rack/utils"
-
1
autoload :Multipart, "rack/multipart"
-
-
1
autoload :MockRequest, "rack/mock"
-
1
autoload :MockResponse, "rack/mock"
-
-
1
autoload :Request, "rack/request"
-
1
autoload :Response, "rack/response"
-
-
1
module Auth
-
1
autoload :Basic, "rack/auth/basic"
-
1
autoload :AbstractRequest, "rack/auth/abstract/request"
-
1
autoload :AbstractHandler, "rack/auth/abstract/handler"
-
1
module Digest
-
1
autoload :MD5, "rack/auth/digest/md5"
-
1
autoload :Nonce, "rack/auth/digest/nonce"
-
1
autoload :Params, "rack/auth/digest/params"
-
1
autoload :Request, "rack/auth/digest/request"
-
end
-
end
-
-
1
module Session
-
1
autoload :Cookie, "rack/session/cookie"
-
1
autoload :Pool, "rack/session/pool"
-
1
autoload :Memcache, "rack/session/memcache"
-
end
-
end